From 8745be8800cb583cbe91fdd91bcf2e19c0121274 Mon Sep 17 00:00:00 2001 From: Kenneth Goossens Date: Wed, 23 Feb 2022 10:00:40 +0100 Subject: [PATCH 1/8] Updates to improve portability. --- .../mutff45/amber14sb_OL15.ff/Makefile.am | 20 + .../mutff45/amber14sb_OL15.ff/Makefile.in | 444 ++ .../mutff45/amber14sb_OL15.ff/aminoacids.arn | 83 + .../amber14sb_OL15.ff/aminoacids.c.tdb | 3 + .../mutff45/amber14sb_OL15.ff/aminoacids.hdb | 518 ++ .../amber14sb_OL15.ff/aminoacids.n.tdb | 3 + .../mutff45/amber14sb_OL15.ff/aminoacids.r2b | 31 + .../mutff45/amber14sb_OL15.ff/aminoacids.rtp | 3401 +++++++++++ .../mutff45/amber14sb_OL15.ff/aminoacids.vsd | 225 + .../mutff45/amber14sb_OL15.ff/atomtypes.atp | 88 + pmx/data/mutff45/amber14sb_OL15.ff/dna.arn | 8 + pmx/data/mutff45/amber14sb_OL15.ff/dna.hdb | 160 + pmx/data/mutff45/amber14sb_OL15.ff/dna.r2b | 6 + pmx/data/mutff45/amber14sb_OL15.ff/dna.rtp | 1223 ++++ .../mutff45/amber14sb_OL15.ff/ffbonded.itp | 1573 +++++ .../mutff45/amber14sb_OL15.ff/ffnonbonded.itp | 97 + .../mutff45/amber14sb_OL15.ff/forcefield.doc | 18 + .../mutff45/amber14sb_OL15.ff/forcefield.itp | 23 + pmx/data/mutff45/amber14sb_OL15.ff/gbsa.itp | 38 + pmx/data/mutff45/amber14sb_OL15.ff/ions.itp | 88 + pmx/data/mutff45/amber14sb_OL15.ff/rna.arn | 8 + pmx/data/mutff45/amber14sb_OL15.ff/rna.hdb | 205 + pmx/data/mutff45/amber14sb_OL15.ff/rna.r2b | 6 + pmx/data/mutff45/amber14sb_OL15.ff/rna.rtp | 1232 ++++ pmx/data/mutff45/amber14sb_OL15.ff/spc.itp | 33 + pmx/data/mutff45/amber14sb_OL15.ff/spce.itp | 33 + pmx/data/mutff45/amber14sb_OL15.ff/tip3p.itp | 34 + pmx/data/mutff45/amber14sb_OL15.ff/tip4p.itp | 55 + .../mutff45/amber14sb_OL15.ff/tip4pew.itp | 56 + pmx/data/mutff45/amber14sb_OL15.ff/tip5p.itp | 69 + pmx/data/mutff45/amber14sb_OL15.ff/urea.itp | 36 + .../mutff45/amber14sb_OL15.ff/watermodels.dat | 5 + pmx/workflow/parmed/__init__.py | 87 + pmx/workflow/parmed/_version.py | 498 ++ pmx/workflow/parmed/amber/__init__.py | 24 + pmx/workflow/parmed/amber/_amberparm.py | 2396 ++++++++ pmx/workflow/parmed/amber/_chamberparm.py | 680 +++ pmx/workflow/parmed/amber/_tinkerparm.py | 1167 ++++ pmx/workflow/parmed/amber/amberformat.py | 955 +++ pmx/workflow/parmed/amber/asciicrd.py | 633 ++ pmx/workflow/parmed/amber/mask.py | 732 +++ pmx/workflow/parmed/amber/mdin/__init__.py | 4 + pmx/workflow/parmed/amber/mdin/cntrl.py | 97 + pmx/workflow/parmed/amber/mdin/ewald.py | 35 + pmx/workflow/parmed/amber/mdin/mdin.py | 304 + pmx/workflow/parmed/amber/mdin/pb.py | 54 + pmx/workflow/parmed/amber/mdin/qmmm.py | 33 + pmx/workflow/parmed/amber/mdin/typing.py | 4 + pmx/workflow/parmed/amber/netcdffiles.py | 766 +++ pmx/workflow/parmed/amber/offlib.py | 630 ++ pmx/workflow/parmed/amber/parameters.py | 813 +++ pmx/workflow/parmed/amber/readparm.py | 72 + .../parmed/amber/titratable_residues.py | 1117 ++++ pmx/workflow/parmed/charmm/__init__.py | 31 + pmx/workflow/parmed/charmm/_charmmfile.py | 186 + pmx/workflow/parmed/charmm/charmmcrds.py | 369 ++ pmx/workflow/parmed/charmm/parameters.py | 1233 ++++ pmx/workflow/parmed/charmm/psf.py | 659 +++ pmx/workflow/parmed/constants.py | 100 + pmx/workflow/parmed/dlpoly/__init__.py | 7 + pmx/workflow/parmed/dlpoly/dlpolyconfig.py | 138 + pmx/workflow/parmed/dlpoly/dlpolyfield.py | 395 ++ pmx/workflow/parmed/entos/__init__.py | 5 + pmx/workflow/parmed/entos/converters.py | 116 + pmx/workflow/parmed/entos/imports.py | 20 + pmx/workflow/parmed/exceptions.py | 120 + pmx/workflow/parmed/formats/__init__.py | 20 + pmx/workflow/parmed/formats/mol2.py | 630 ++ pmx/workflow/parmed/formats/pdb.py | 1760 ++++++ .../parmed/formats/pdbx/PdbxContainers.py | 806 +++ .../parmed/formats/pdbx/PdbxReader.py | 447 ++ .../parmed/formats/pdbx/PdbxWriter.py | 187 + pmx/workflow/parmed/formats/pdbx/__init__.py | 17 + pmx/workflow/parmed/formats/pqr.py | 313 + pmx/workflow/parmed/formats/psf.py | 285 + pmx/workflow/parmed/formats/registry.py | 217 + pmx/workflow/parmed/formats/sdf.py | 32 + pmx/workflow/parmed/geometry.py | 370 ++ pmx/workflow/parmed/gromacs/__init__.py | 45 + pmx/workflow/parmed/gromacs/_cpp.py | 379 ++ pmx/workflow/parmed/gromacs/_gromacsfile.py | 108 + pmx/workflow/parmed/gromacs/gromacsgro.py | 312 + pmx/workflow/parmed/gromacs/gromacstop.py | 2100 +++++++ pmx/workflow/parmed/modeller/__init__.py | 11 + .../modeller/data/standard_residues.lib | 5244 ++++++++++++++++ pmx/workflow/parmed/modeller/residue.py | 999 ++++ .../parmed/modeller/standardtemplates.py | 10 + pmx/workflow/parmed/namd/__init__.py | 12 + pmx/workflow/parmed/namd/namdbinfiles.py | 110 + pmx/workflow/parmed/openmm/__init__.py | 17 + pmx/workflow/parmed/openmm/parameters.py | 1208 ++++ pmx/workflow/parmed/openmm/reporters.py | 886 +++ pmx/workflow/parmed/openmm/topsystem.py | 435 ++ pmx/workflow/parmed/openmm/utils.py | 110 + pmx/workflow/parmed/openmm/xmlfile.py | 163 + pmx/workflow/parmed/parameters.py | 490 ++ pmx/workflow/parmed/periodic_table.py | 306 + pmx/workflow/parmed/rdkit/__init__.py | 10 + pmx/workflow/parmed/rdkit/rdkit.py | 80 + pmx/workflow/parmed/residue.py | 286 + pmx/workflow/parmed/rosetta/__init__.py | 9 + pmx/workflow/parmed/rosetta/pose.py | 84 + pmx/workflow/parmed/scripts.py | 200 + pmx/workflow/parmed/structure.py | 4060 +++++++++++++ pmx/workflow/parmed/symmetry.py | 13 + pmx/workflow/parmed/tinker/__init__.py | 8 + pmx/workflow/parmed/tinker/parameterfile.py | 552 ++ pmx/workflow/parmed/tinker/system.py | 477 ++ pmx/workflow/parmed/tinker/tinkerfiles.py | 298 + pmx/workflow/parmed/tinker/topologyobjects.py | 353 ++ pmx/workflow/parmed/tools/__init__.py | 9 + pmx/workflow/parmed/tools/actions.py | 4289 ++++++++++++++ pmx/workflow/parmed/tools/add1264.py | 164 + pmx/workflow/parmed/tools/addljtype.py | 81 + pmx/workflow/parmed/tools/argumentlist.py | 233 + pmx/workflow/parmed/tools/changeradii.py | 1031 ++++ pmx/workflow/parmed/tools/checkvalidity.py | 280 + pmx/workflow/parmed/tools/exceptions.py | 189 + pmx/workflow/parmed/tools/logos.py | 211 + pmx/workflow/parmed/tools/parmed_cmd.py | 468 ++ pmx/workflow/parmed/tools/parmlist.py | 86 + .../parmed/tools/simulations/__init__.py | 4 + .../parmed/tools/simulations/openmm.py | 1067 ++++ .../parmed/tools/simulations/sanderapi.py | 141 + pmx/workflow/parmed/topologyobjects.py | 5269 +++++++++++++++++ pmx/workflow/parmed/unit/__init__.py | 47 + pmx/workflow/parmed/unit/basedimension.py | 116 + pmx/workflow/parmed/unit/baseunit.py | 170 + pmx/workflow/parmed/unit/constants.py | 54 + pmx/workflow/parmed/unit/mymatrix.py | 468 ++ pmx/workflow/parmed/unit/prefix.py | 182 + pmx/workflow/parmed/unit/quantity.py | 827 +++ .../parmed/unit/standard_dimensions.py | 61 + pmx/workflow/parmed/unit/unit.py | 720 +++ pmx/workflow/parmed/unit/unit_definitions.py | 359 ++ pmx/workflow/parmed/unit/unit_math.py | 194 + pmx/workflow/parmed/unit/unit_operators.py | 145 + pmx/workflow/parmed/utils/__init__.py | 53 + pmx/workflow/parmed/utils/decorators.py | 29 + .../fortranformat/FortranRecordReader.py | 66 + .../fortranformat/FortranRecordWriter.py | 56 + .../parmed/utils/fortranformat/__init__.py | 31 + .../utils/fortranformat/_edit_descriptors.py | 301 + .../parmed/utils/fortranformat/_exceptions.py | 3 + .../parmed/utils/fortranformat/_input.py | 346 ++ .../parmed/utils/fortranformat/_lexer.py | 178 + .../parmed/utils/fortranformat/_misc.py | 47 + .../parmed/utils/fortranformat/_output.py | 708 +++ .../parmed/utils/fortranformat/_parser.py | 381 ++ .../utils/fortranformat/check_version.txt | 3 + .../parmed/utils/fortranformat/config.py | 64 + pmx/workflow/parmed/utils/io.py | 95 + pmx/workflow/parmed/utils/netcdf.py | 1091 ++++ pmx/workflow/parmed/utils/pairlist.py | 56 + pmx/workflow/parmed/utils/pandautils.py | 359 ++ pmx/workflow/parmed/utils/timer.py | 91 + pmx/workflow/parmed/vec3.py | 92 + pmx/workflow/pmxworkflow.py | 2 +- pmx/workflow/sge.py | 18 +- .../workflow1_parameterize_ligands.py | 8 +- pmx/workflow/workflow2_make_perturbations.py | 12 +- pmx/workflow/workflow3_solvate.py | 55 +- pmx/workflow/workflow4_write_scripts.py | 6 +- pmx/workflow/workflow5_submit_simulations.py | 6 +- pmx/workflow/workflow6_check_simulations.py | 6 +- 165 files changed, 69754 insertions(+), 35 deletions(-) create mode 100644 pmx/data/mutff45/amber14sb_OL15.ff/Makefile.am create mode 100644 pmx/data/mutff45/amber14sb_OL15.ff/Makefile.in create mode 100644 pmx/data/mutff45/amber14sb_OL15.ff/aminoacids.arn create mode 100644 pmx/data/mutff45/amber14sb_OL15.ff/aminoacids.c.tdb create mode 100644 pmx/data/mutff45/amber14sb_OL15.ff/aminoacids.hdb create mode 100644 pmx/data/mutff45/amber14sb_OL15.ff/aminoacids.n.tdb create mode 100644 pmx/data/mutff45/amber14sb_OL15.ff/aminoacids.r2b create mode 100644 pmx/data/mutff45/amber14sb_OL15.ff/aminoacids.rtp create mode 100644 pmx/data/mutff45/amber14sb_OL15.ff/aminoacids.vsd create mode 100644 pmx/data/mutff45/amber14sb_OL15.ff/atomtypes.atp create mode 100644 pmx/data/mutff45/amber14sb_OL15.ff/dna.arn create mode 100644 pmx/data/mutff45/amber14sb_OL15.ff/dna.hdb create mode 100644 pmx/data/mutff45/amber14sb_OL15.ff/dna.r2b create mode 100644 pmx/data/mutff45/amber14sb_OL15.ff/dna.rtp create mode 100644 pmx/data/mutff45/amber14sb_OL15.ff/ffbonded.itp create mode 100644 pmx/data/mutff45/amber14sb_OL15.ff/ffnonbonded.itp create mode 100644 pmx/data/mutff45/amber14sb_OL15.ff/forcefield.doc create mode 100644 pmx/data/mutff45/amber14sb_OL15.ff/forcefield.itp create mode 100644 pmx/data/mutff45/amber14sb_OL15.ff/gbsa.itp create mode 100644 pmx/data/mutff45/amber14sb_OL15.ff/ions.itp create mode 100644 pmx/data/mutff45/amber14sb_OL15.ff/rna.arn create mode 100644 pmx/data/mutff45/amber14sb_OL15.ff/rna.hdb create mode 100644 pmx/data/mutff45/amber14sb_OL15.ff/rna.r2b create mode 100644 pmx/data/mutff45/amber14sb_OL15.ff/rna.rtp create mode 100644 pmx/data/mutff45/amber14sb_OL15.ff/spc.itp create mode 100644 pmx/data/mutff45/amber14sb_OL15.ff/spce.itp create mode 100644 pmx/data/mutff45/amber14sb_OL15.ff/tip3p.itp create mode 100644 pmx/data/mutff45/amber14sb_OL15.ff/tip4p.itp create mode 100644 pmx/data/mutff45/amber14sb_OL15.ff/tip4pew.itp create mode 100644 pmx/data/mutff45/amber14sb_OL15.ff/tip5p.itp create mode 100644 pmx/data/mutff45/amber14sb_OL15.ff/urea.itp create mode 100644 pmx/data/mutff45/amber14sb_OL15.ff/watermodels.dat create mode 100644 pmx/workflow/parmed/__init__.py create mode 100644 pmx/workflow/parmed/_version.py create mode 100644 pmx/workflow/parmed/amber/__init__.py create mode 100644 pmx/workflow/parmed/amber/_amberparm.py create mode 100644 pmx/workflow/parmed/amber/_chamberparm.py create mode 100644 pmx/workflow/parmed/amber/_tinkerparm.py create mode 100644 pmx/workflow/parmed/amber/amberformat.py create mode 100644 pmx/workflow/parmed/amber/asciicrd.py create mode 100644 pmx/workflow/parmed/amber/mask.py create mode 100644 pmx/workflow/parmed/amber/mdin/__init__.py create mode 100644 pmx/workflow/parmed/amber/mdin/cntrl.py create mode 100644 pmx/workflow/parmed/amber/mdin/ewald.py create mode 100644 pmx/workflow/parmed/amber/mdin/mdin.py create mode 100644 pmx/workflow/parmed/amber/mdin/pb.py create mode 100644 pmx/workflow/parmed/amber/mdin/qmmm.py create mode 100644 pmx/workflow/parmed/amber/mdin/typing.py create mode 100644 pmx/workflow/parmed/amber/netcdffiles.py create mode 100644 pmx/workflow/parmed/amber/offlib.py create mode 100644 pmx/workflow/parmed/amber/parameters.py create mode 100644 pmx/workflow/parmed/amber/readparm.py create mode 100644 pmx/workflow/parmed/amber/titratable_residues.py create mode 100644 pmx/workflow/parmed/charmm/__init__.py create mode 100644 pmx/workflow/parmed/charmm/_charmmfile.py create mode 100644 pmx/workflow/parmed/charmm/charmmcrds.py create mode 100644 pmx/workflow/parmed/charmm/parameters.py create mode 100644 pmx/workflow/parmed/charmm/psf.py create mode 100644 pmx/workflow/parmed/constants.py create mode 100644 pmx/workflow/parmed/dlpoly/__init__.py create mode 100644 pmx/workflow/parmed/dlpoly/dlpolyconfig.py create mode 100644 pmx/workflow/parmed/dlpoly/dlpolyfield.py create mode 100644 pmx/workflow/parmed/entos/__init__.py create mode 100644 pmx/workflow/parmed/entos/converters.py create mode 100644 pmx/workflow/parmed/entos/imports.py create mode 100644 pmx/workflow/parmed/exceptions.py create mode 100644 pmx/workflow/parmed/formats/__init__.py create mode 100644 pmx/workflow/parmed/formats/mol2.py create mode 100644 pmx/workflow/parmed/formats/pdb.py create mode 100644 pmx/workflow/parmed/formats/pdbx/PdbxContainers.py create mode 100644 pmx/workflow/parmed/formats/pdbx/PdbxReader.py create mode 100644 pmx/workflow/parmed/formats/pdbx/PdbxWriter.py create mode 100644 pmx/workflow/parmed/formats/pdbx/__init__.py create mode 100644 pmx/workflow/parmed/formats/pqr.py create mode 100644 pmx/workflow/parmed/formats/psf.py create mode 100644 pmx/workflow/parmed/formats/registry.py create mode 100644 pmx/workflow/parmed/formats/sdf.py create mode 100644 pmx/workflow/parmed/geometry.py create mode 100644 pmx/workflow/parmed/gromacs/__init__.py create mode 100644 pmx/workflow/parmed/gromacs/_cpp.py create mode 100644 pmx/workflow/parmed/gromacs/_gromacsfile.py create mode 100644 pmx/workflow/parmed/gromacs/gromacsgro.py create mode 100644 pmx/workflow/parmed/gromacs/gromacstop.py create mode 100644 pmx/workflow/parmed/modeller/__init__.py create mode 100644 pmx/workflow/parmed/modeller/data/standard_residues.lib create mode 100644 pmx/workflow/parmed/modeller/residue.py create mode 100644 pmx/workflow/parmed/modeller/standardtemplates.py create mode 100644 pmx/workflow/parmed/namd/__init__.py create mode 100644 pmx/workflow/parmed/namd/namdbinfiles.py create mode 100644 pmx/workflow/parmed/openmm/__init__.py create mode 100644 pmx/workflow/parmed/openmm/parameters.py create mode 100644 pmx/workflow/parmed/openmm/reporters.py create mode 100644 pmx/workflow/parmed/openmm/topsystem.py create mode 100644 pmx/workflow/parmed/openmm/utils.py create mode 100644 pmx/workflow/parmed/openmm/xmlfile.py create mode 100644 pmx/workflow/parmed/parameters.py create mode 100644 pmx/workflow/parmed/periodic_table.py create mode 100644 pmx/workflow/parmed/rdkit/__init__.py create mode 100644 pmx/workflow/parmed/rdkit/rdkit.py create mode 100644 pmx/workflow/parmed/residue.py create mode 100644 pmx/workflow/parmed/rosetta/__init__.py create mode 100644 pmx/workflow/parmed/rosetta/pose.py create mode 100644 pmx/workflow/parmed/scripts.py create mode 100644 pmx/workflow/parmed/structure.py create mode 100644 pmx/workflow/parmed/symmetry.py create mode 100644 pmx/workflow/parmed/tinker/__init__.py create mode 100644 pmx/workflow/parmed/tinker/parameterfile.py create mode 100644 pmx/workflow/parmed/tinker/system.py create mode 100644 pmx/workflow/parmed/tinker/tinkerfiles.py create mode 100644 pmx/workflow/parmed/tinker/topologyobjects.py create mode 100644 pmx/workflow/parmed/tools/__init__.py create mode 100644 pmx/workflow/parmed/tools/actions.py create mode 100644 pmx/workflow/parmed/tools/add1264.py create mode 100644 pmx/workflow/parmed/tools/addljtype.py create mode 100644 pmx/workflow/parmed/tools/argumentlist.py create mode 100644 pmx/workflow/parmed/tools/changeradii.py create mode 100644 pmx/workflow/parmed/tools/checkvalidity.py create mode 100644 pmx/workflow/parmed/tools/exceptions.py create mode 100644 pmx/workflow/parmed/tools/logos.py create mode 100644 pmx/workflow/parmed/tools/parmed_cmd.py create mode 100644 pmx/workflow/parmed/tools/parmlist.py create mode 100644 pmx/workflow/parmed/tools/simulations/__init__.py create mode 100644 pmx/workflow/parmed/tools/simulations/openmm.py create mode 100644 pmx/workflow/parmed/tools/simulations/sanderapi.py create mode 100644 pmx/workflow/parmed/topologyobjects.py create mode 100644 pmx/workflow/parmed/unit/__init__.py create mode 100644 pmx/workflow/parmed/unit/basedimension.py create mode 100644 pmx/workflow/parmed/unit/baseunit.py create mode 100644 pmx/workflow/parmed/unit/constants.py create mode 100644 pmx/workflow/parmed/unit/mymatrix.py create mode 100644 pmx/workflow/parmed/unit/prefix.py create mode 100644 pmx/workflow/parmed/unit/quantity.py create mode 100644 pmx/workflow/parmed/unit/standard_dimensions.py create mode 100644 pmx/workflow/parmed/unit/unit.py create mode 100644 pmx/workflow/parmed/unit/unit_definitions.py create mode 100644 pmx/workflow/parmed/unit/unit_math.py create mode 100644 pmx/workflow/parmed/unit/unit_operators.py create mode 100644 pmx/workflow/parmed/utils/__init__.py create mode 100644 pmx/workflow/parmed/utils/decorators.py create mode 100644 pmx/workflow/parmed/utils/fortranformat/FortranRecordReader.py create mode 100644 pmx/workflow/parmed/utils/fortranformat/FortranRecordWriter.py create mode 100644 pmx/workflow/parmed/utils/fortranformat/__init__.py create mode 100644 pmx/workflow/parmed/utils/fortranformat/_edit_descriptors.py create mode 100644 pmx/workflow/parmed/utils/fortranformat/_exceptions.py create mode 100644 pmx/workflow/parmed/utils/fortranformat/_input.py create mode 100644 pmx/workflow/parmed/utils/fortranformat/_lexer.py create mode 100644 pmx/workflow/parmed/utils/fortranformat/_misc.py create mode 100644 pmx/workflow/parmed/utils/fortranformat/_output.py create mode 100644 pmx/workflow/parmed/utils/fortranformat/_parser.py create mode 100644 pmx/workflow/parmed/utils/fortranformat/check_version.txt create mode 100644 pmx/workflow/parmed/utils/fortranformat/config.py create mode 100644 pmx/workflow/parmed/utils/io.py create mode 100644 pmx/workflow/parmed/utils/netcdf.py create mode 100644 pmx/workflow/parmed/utils/pairlist.py create mode 100644 pmx/workflow/parmed/utils/pandautils.py create mode 100644 pmx/workflow/parmed/utils/timer.py create mode 100644 pmx/workflow/parmed/vec3.py diff --git a/pmx/data/mutff45/amber14sb_OL15.ff/Makefile.am b/pmx/data/mutff45/amber14sb_OL15.ff/Makefile.am new file mode 100644 index 00000000..f2c53bd9 --- /dev/null +++ b/pmx/data/mutff45/amber14sb_OL15.ff/Makefile.am @@ -0,0 +1,20 @@ +## Process this file with automake to produce Makefile.in +# +# Don't edit - this file is generated automatically from Makefile.am +# +topoldir = ${pkgdatadir}/top/amber14sb.ff + +topol_DATA = \ + aminoacids.arn aminoacids.vsd \ + aminoacids.c.tdb atomtypes.atp ffnonbonded.itp \ + aminoacids.hdb forcefield.doc dna.rtp dna.r2b dna.arn dna.hdb \ + aminoacids.n.tdb forcefield.itp rna.rtp rna.r2b rna.arn rna.hdb \ + aminoacids.r2b ffbonded.itp tip3p.itp urea.itp \ + aminoacids.rtp ions.itp tip4p.itp \ + tip4pew.itp tip5p.itp watermodels.dat gbsa.itp \ + spc.itp spce.itp + +EXTRA_DIST = ${topol_DATA} + +CLEANFILES = *~ \\\#* + diff --git a/pmx/data/mutff45/amber14sb_OL15.ff/Makefile.in b/pmx/data/mutff45/amber14sb_OL15.ff/Makefile.in new file mode 100644 index 00000000..3b4960f6 --- /dev/null +++ b/pmx/data/mutff45/amber14sb_OL15.ff/Makefile.in @@ -0,0 +1,444 @@ +# Makefile.in generated by automake 1.11 from Makefile.am. +# @configure_input@ + +# Copyright (C) 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, +# 2003, 2004, 2005, 2006, 2007, 2008, 2009 Free Software Foundation, +# Inc. +# This Makefile.in is free software; the Free Software Foundation +# gives unlimited permission to copy and/or distribute it, +# with or without modifications, as long as this notice is preserved. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY, to the extent permitted by law; without +# even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. + +@SET_MAKE@ + +VPATH = @srcdir@ +pkgdatadir = $(datadir)/@PACKAGE@ +pkgincludedir = $(includedir)/@PACKAGE@ +pkglibdir = $(libdir)/@PACKAGE@ +pkglibexecdir = $(libexecdir)/@PACKAGE@ +am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd +install_sh_DATA = $(install_sh) -c -m 644 +install_sh_PROGRAM = $(install_sh) -c +install_sh_SCRIPT = $(install_sh) -c +INSTALL_HEADER = $(INSTALL_DATA) +transform = $(program_transform_name) +NORMAL_INSTALL = : +PRE_INSTALL = : +POST_INSTALL = : +NORMAL_UNINSTALL = : +PRE_UNINSTALL = : +POST_UNINSTALL = : +build_triplet = @build@ +host_triplet = @host@ +subdir = share/top/amber14sb.ff +DIST_COMMON = $(srcdir)/Makefile.am $(srcdir)/Makefile.in +ACLOCAL_M4 = $(top_srcdir)/aclocal.m4 +am__aclocal_m4_deps = $(top_srcdir)/acinclude.m4 \ + $(top_srcdir)/configure.ac +am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \ + $(ACLOCAL_M4) +mkinstalldirs = $(SHELL) $(top_srcdir)/config/mkinstalldirs +CONFIG_HEADER = $(top_builddir)/src/config.h +CONFIG_CLEAN_FILES = +CONFIG_CLEAN_VPATH_FILES = +SOURCES = +DIST_SOURCES = +am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`; +am__vpath_adj = case $$p in \ + $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \ + *) f=$$p;; \ + esac; +am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`; +am__install_max = 40 +am__nobase_strip_setup = \ + srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'` +am__nobase_strip = \ + for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||" +am__nobase_list = $(am__nobase_strip_setup); \ + for p in $$list; do echo "$$p $$p"; done | \ + sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \ + $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \ + if (++n[$$2] == $(am__install_max)) \ + { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \ + END { for (dir in files) print dir, files[dir] }' +am__base_list = \ + sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \ + sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g' +am__installdirs = "$(DESTDIR)$(topoldir)" +DATA = $(topol_DATA) +DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST) +ACLOCAL = @ACLOCAL@ +AMTAR = @AMTAR@ +AR = @AR@ +AS = @AS@ +ASFLAGS = @ASFLAGS@ +AUTOCONF = @AUTOCONF@ +AUTOHEADER = @AUTOHEADER@ +AUTOMAKE = @AUTOMAKE@ +AWK = @AWK@ +CC = @CC@ +CCAS = @CCAS@ +CCASDEPMODE = @CCASDEPMODE@ +CCASFLAGS = @CCASFLAGS@ +CCDEPMODE = @CCDEPMODE@ +CC_FOR_BUILD = @CC_FOR_BUILD@ +CFLAGS = @CFLAGS@ +CPP = @CPP@ +CPPFLAGS = @CPPFLAGS@ +CXX = @CXX@ +CXXCPP = @CXXCPP@ +CXXDEPMODE = @CXXDEPMODE@ +CXXFLAGS = @CXXFLAGS@ +CYGPATH_W = @CYGPATH_W@ +DEFS = @DEFS@ +DEPDIR = @DEPDIR@ +DLLTOOL = @DLLTOOL@ +DLOPEN_LIBS = @DLOPEN_LIBS@ +ECHO = @ECHO@ +ECHO_C = @ECHO_C@ +ECHO_N = @ECHO_N@ +ECHO_T = @ECHO_T@ +EGREP = @EGREP@ +EXEEXT = @EXEEXT@ +F77 = @F77@ +F77LINK = @F77LINK@ +FFLAGS = @FFLAGS@ +FFT_LIBS = @FFT_LIBS@ +FLIBS = @FLIBS@ +GREP = @GREP@ +GSL_LIBS = @GSL_LIBS@ +INCLUDES = @INCLUDES@ +INSTALL = @INSTALL@ +INSTALL_DATA = @INSTALL_DATA@ +INSTALL_PROGRAM = @INSTALL_PROGRAM@ +INSTALL_SCRIPT = @INSTALL_SCRIPT@ +INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@ +LDFLAGS = @LDFLAGS@ +LIBOBJS = @LIBOBJS@ +LIBS = @LIBS@ +LIBSUFFIX = @LIBSUFFIX@ +LIBTOOL = @LIBTOOL@ +LMKL = @LMKL@ +LN_S = @LN_S@ +LTLIBOBJS = @LTLIBOBJS@ +MAKEINFO = @MAKEINFO@ +MKDIR_P = @MKDIR_P@ +MPICC = @MPICC@ +OBJDUMP = @OBJDUMP@ +OBJEXT = @OBJEXT@ +PACKAGE = @PACKAGE@ +PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@ +PACKAGE_NAME = @PACKAGE_NAME@ +PACKAGE_STRING = @PACKAGE_STRING@ +PACKAGE_TARNAME = @PACKAGE_TARNAME@ +PACKAGE_URL = @PACKAGE_URL@ +PACKAGE_VERSION = @PACKAGE_VERSION@ +PATH_SEPARATOR = @PATH_SEPARATOR@ +PKG_CFLAGS = @PKG_CFLAGS@ +PKG_FFT = @PKG_FFT@ +PKG_FFT_LIBS = @PKG_FFT_LIBS@ +PKG_GSL = @PKG_GSL@ +PKG_XML = @PKG_XML@ +PTHREAD_CC = @PTHREAD_CC@ +PTHREAD_CFLAGS = @PTHREAD_CFLAGS@ +PTHREAD_LIBS = @PTHREAD_LIBS@ +RANLIB = @RANLIB@ +RPM = @RPM@ +SET_MAKE = @SET_MAKE@ +SHARED_VERSION_INFO = @SHARED_VERSION_INFO@ +SHELL = @SHELL@ +STRIP = @STRIP@ +VERSION = @VERSION@ +XMKMF = @XMKMF@ +XML_LIBS = @XML_LIBS@ +X_CFLAGS = @X_CFLAGS@ +X_EXTRA_LIBS = @X_EXTRA_LIBS@ +X_LIBS = @X_LIBS@ +X_PRE_LIBS = @X_PRE_LIBS@ +abs_builddir = @abs_builddir@ +abs_srcdir = @abs_srcdir@ +abs_top_builddir = @abs_top_builddir@ +abs_top_srcdir = @abs_top_srcdir@ +ac_ct_CC = @ac_ct_CC@ +ac_ct_CXX = @ac_ct_CXX@ +ac_ct_F77 = @ac_ct_F77@ +am__include = @am__include@ +am__leading_dot = @am__leading_dot@ +am__quote = @am__quote@ +am__tar = @am__tar@ +am__untar = @am__untar@ +bindir = @bindir@ +build = @build@ +build_alias = @build_alias@ +build_cpu = @build_cpu@ +build_os = @build_os@ +build_vendor = @build_vendor@ +builddir = @builddir@ +datadir = @datadir@ +datarootdir = @datarootdir@ +docdir = @docdir@ +dvidir = @dvidir@ +exec_prefix = @exec_prefix@ +have_git = @have_git@ +host = @host@ +host_alias = @host_alias@ +host_cpu = @host_cpu@ +host_os = @host_os@ +host_vendor = @host_vendor@ +htmldir = @htmldir@ +includedir = @includedir@ +infodir = @infodir@ +install_sh = @install_sh@ +libdir = @libdir@ +libexecdir = @libexecdir@ +localedir = @localedir@ +localstatedir = @localstatedir@ +mandir = @mandir@ +mkdir_p = @mkdir_p@ +oldincludedir = @oldincludedir@ +pdfdir = @pdfdir@ +prefix = @prefix@ +program_transform_name = @program_transform_name@ +psdir = @psdir@ +sbindir = @sbindir@ +sharedstatedir = @sharedstatedir@ +srcdir = @srcdir@ +sysconfdir = @sysconfdir@ +target_alias = @target_alias@ +top_build_prefix = @top_build_prefix@ +top_builddir = @top_builddir@ +top_srcdir = @top_srcdir@ + +# +# Don't edit - this file is generated automatically from Makefile.am +# +topoldir = ${pkgdatadir}/top/amber14sb.ff +topol_DATA = \ + aminoacids.arn aminoacids.vsd \ + aminoacids.c.tdb atomtypes.atp ffnonbonded.itp \ + aminoacids.hdb forcefield.doc dna.rtp dna.r2b dna.arn dna.hdb \ + aminoacids.n.tdb forcefield.itp rna.rtp rna.r2b rna.arn rna.hdb \ + aminoacids.r2b ffbonded.itp tip3p.itp urea.itp \ + aminoacids.rtp ions.itp tip4p.itp \ + tip4pew.itp tip5p.itp watermodels.dat gbsa.itp \ + spc.itp spce.itp + +EXTRA_DIST = ${topol_DATA} +CLEANFILES = *~ \\\#* +all: all-am + +.SUFFIXES: +$(srcdir)/Makefile.in: $(srcdir)/Makefile.am $(am__configure_deps) + @for dep in $?; do \ + case '$(am__configure_deps)' in \ + *$$dep*) \ + ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \ + && { if test -f $@; then exit 0; else break; fi; }; \ + exit 1;; \ + esac; \ + done; \ + echo ' cd $(top_srcdir) && $(AUTOMAKE) --gnu share/top/amber14sb.ff/Makefile'; \ + $(am__cd) $(top_srcdir) && \ + $(AUTOMAKE) --gnu share/top/amber14sb.ff/Makefile +.PRECIOUS: Makefile +Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status + @case '$?' in \ + *config.status*) \ + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \ + *) \ + echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__depfiles_maybe)'; \ + cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__depfiles_maybe);; \ + esac; + +$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh + +$(top_srcdir)/configure: $(am__configure_deps) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh +$(ACLOCAL_M4): $(am__aclocal_m4_deps) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh +$(am__aclocal_m4_deps): + +mostlyclean-libtool: + -rm -f *.lo + +clean-libtool: + -rm -rf .libs _libs +install-topolDATA: $(topol_DATA) + @$(NORMAL_INSTALL) + test -z "$(topoldir)" || $(MKDIR_P) "$(DESTDIR)$(topoldir)" + @list='$(topol_DATA)'; test -n "$(topoldir)" || list=; \ + for p in $$list; do \ + if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \ + echo "$$d$$p"; \ + done | $(am__base_list) | \ + while read files; do \ + echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(topoldir)'"; \ + $(INSTALL_DATA) $$files "$(DESTDIR)$(topoldir)" || exit $$?; \ + done + +uninstall-topolDATA: + @$(NORMAL_UNINSTALL) + @list='$(topol_DATA)'; test -n "$(topoldir)" || list=; \ + files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \ + test -n "$$files" || exit 0; \ + echo " ( cd '$(DESTDIR)$(topoldir)' && rm -f" $$files ")"; \ + cd "$(DESTDIR)$(topoldir)" && rm -f $$files +tags: TAGS +TAGS: + +ctags: CTAGS +CTAGS: + + +distdir: $(DISTFILES) + @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ + topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ + list='$(DISTFILES)'; \ + dist_files=`for file in $$list; do echo $$file; done | \ + sed -e "s|^$$srcdirstrip/||;t" \ + -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \ + case $$dist_files in \ + */*) $(MKDIR_P) `echo "$$dist_files" | \ + sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \ + sort -u` ;; \ + esac; \ + for file in $$dist_files; do \ + if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \ + if test -d $$d/$$file; then \ + dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \ + if test -d "$(distdir)/$$file"; then \ + find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ + fi; \ + if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \ + cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \ + find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ + fi; \ + cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \ + else \ + test -f "$(distdir)/$$file" \ + || cp -p $$d/$$file "$(distdir)/$$file" \ + || exit 1; \ + fi; \ + done +check-am: all-am +check: check-am +all-am: Makefile $(DATA) +installdirs: + for dir in "$(DESTDIR)$(topoldir)"; do \ + test -z "$$dir" || $(MKDIR_P) "$$dir"; \ + done +install: install-am +install-exec: install-exec-am +install-data: install-data-am +uninstall: uninstall-am + +install-am: all-am + @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am + +installcheck: installcheck-am +install-strip: + $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ + install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ + `test -z '$(STRIP)' || \ + echo "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'"` install +mostlyclean-generic: + +clean-generic: + -test -z "$(CLEANFILES)" || rm -f $(CLEANFILES) + +distclean-generic: + -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES) + -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES) + +maintainer-clean-generic: + @echo "This command is intended for maintainers to use" + @echo "it deletes files that may require special tools to rebuild." +clean: clean-am + +clean-am: clean-generic clean-libtool mostlyclean-am + +distclean: distclean-am + -rm -f Makefile +distclean-am: clean-am distclean-generic + +dvi: dvi-am + +dvi-am: + +html: html-am + +html-am: + +info: info-am + +info-am: + +install-data-am: install-topolDATA + +install-dvi: install-dvi-am + +install-dvi-am: + +install-exec-am: + +install-html: install-html-am + +install-html-am: + +install-info: install-info-am + +install-info-am: + +install-man: + +install-pdf: install-pdf-am + +install-pdf-am: + +install-ps: install-ps-am + +install-ps-am: + +installcheck-am: + +maintainer-clean: maintainer-clean-am + -rm -f Makefile +maintainer-clean-am: distclean-am maintainer-clean-generic + +mostlyclean: mostlyclean-am + +mostlyclean-am: mostlyclean-generic mostlyclean-libtool + +pdf: pdf-am + +pdf-am: + +ps: ps-am + +ps-am: + +uninstall-am: uninstall-topolDATA + +.MAKE: install-am install-strip + +.PHONY: all all-am check check-am clean clean-generic clean-libtool \ + distclean distclean-generic distclean-libtool distdir dvi \ + dvi-am html html-am info info-am install install-am \ + install-data install-data-am install-dvi install-dvi-am \ + install-exec install-exec-am install-html install-html-am \ + install-info install-info-am install-man install-pdf \ + install-pdf-am install-ps install-ps-am install-strip \ + install-topolDATA installcheck installcheck-am installdirs \ + maintainer-clean maintainer-clean-generic mostlyclean \ + mostlyclean-generic mostlyclean-libtool pdf pdf-am ps ps-am \ + uninstall uninstall-am uninstall-topolDATA + + +# Tell versions [3.59,3.63) of GNU make to not export all variables. +# Otherwise a system limit (for SysV at least) may be exceeded. +.NOEXPORT: diff --git a/pmx/data/mutff45/amber14sb_OL15.ff/aminoacids.arn b/pmx/data/mutff45/amber14sb_OL15.ff/aminoacids.arn new file mode 100644 index 00000000..5da55eab --- /dev/null +++ b/pmx/data/mutff45/amber14sb_OL15.ff/aminoacids.arn @@ -0,0 +1,83 @@ +; atom renaming specification +; residue gromacs forcefield + NALA H H1 + NGLY H H1 + NSER H H1 + NTHR H H1 + NLEU H H1 + NILE H H1 + NVAL H H1 + NASN H H1 + NGLN H H1 + NARG H H1 + NHID H H1 + NHIE H H1 + NHIP H H1 + NTRP H H1 + NPHE H H1 + NTYR H H1 + NGLU H H1 + NASP H H1 + NLYS H H1 + NPRO H H1 + NCYS H H1 + NCYX H H1 + NMET H H1 + NGLH H H1 + NASP H H1 +; This mapping of O->OC2 and OXT->OC1 might look a bit strange, +; but when we rebuild an oxygen it will be placed directly after C, so +; this mapping makes sure OC1 comes before OC2. The oxygens are anyway chemically +; equivalent, and if you already had their coordinates present they will be kept intact. + CALA O OC2 + CALA OXT OC1 + CGLY O OC2 + CGLY OXT OC1 + CSER O OC2 + CSER OXT OC1 + CTHR O OC2 + CTHR OXT OC1 + CLEU O OC2 + CLEU OXT OC1 + CILE O OC2 + CILE OXT OC1 + CVAL O OC2 + CVAL OXT OC1 + CASN O OC2 + CASN OXT OC1 + CGLN O OC2 + CGLN OXT OC1 + CARG O OC2 + CARG OXT OC1 + CHID O OC2 + CHID OXT OC1 + CHIE O OC2 + CHIE OXT OC1 + CHIP O OC2 + CHIP OXT OC1 + CTRP O OC2 + CTRP OXT OC1 + CPHE O OC2 + CPHE OXT OC1 + CTYR O OC2 + CTYR OXT OC1 + CGLU O OC2 + CGLU OXT OC1 + CASP O OC2 + CASP OXT OC1 + CLYS O OC2 + CLYS OXT OC1 + CPRO O OC2 + CPRO OXT OC1 + CCYS O OC2 + CCYS OXT OC1 + CCYX O OC2 + CCYX OXT OC1 + CMET O OC2 + CMET OXT OC1 + CGLH O OC2 + CGLH OXT OC1 + CASH O OC2 + CASH OXT OC1 + + diff --git a/pmx/data/mutff45/amber14sb_OL15.ff/aminoacids.c.tdb b/pmx/data/mutff45/amber14sb_OL15.ff/aminoacids.c.tdb new file mode 100644 index 00000000..50d0742f --- /dev/null +++ b/pmx/data/mutff45/amber14sb_OL15.ff/aminoacids.c.tdb @@ -0,0 +1,3 @@ +; empty + + diff --git a/pmx/data/mutff45/amber14sb_OL15.ff/aminoacids.hdb b/pmx/data/mutff45/amber14sb_OL15.ff/aminoacids.hdb new file mode 100644 index 00000000..4df98139 --- /dev/null +++ b/pmx/data/mutff45/amber14sb_OL15.ff/aminoacids.hdb @@ -0,0 +1,518 @@ +HOH 1 +2 7 HW OW +HO4 1 +3 10 HW OW +ACE 1 +3 4 HH3 CH3 C O +NME 2 +1 1 H N -C CH3 +3 4 HH3 CH3 N -C +NHE 1 +2 3 H N -C -CA +NH2 1 +2 3 H N -C -CA +ALA 3 +1 1 H N -C CA +1 5 HA CA N CB C +3 4 HB CB CA N +GLY 2 +1 1 H N -C CA +2 6 HA CA N C +SER 4 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA OG +1 2 HG OG CB CA +THR 5 +1 1 H N -C CA +1 5 HA CA N CB C +1 5 HB CB CA CG2 OG1 +3 4 HG2 CG2 CB CA +1 2 HG1 OG1 CB CA +LEU 6 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA CG +1 5 HG CG CB CD1 CD2 +3 4 HD1 CD1 CG CB +3 4 HD2 CD2 CG CB +ILE 6 +1 1 H N -C CA +1 5 HA CA N CB C +1 5 HB CB CA CG2 CG1 +3 4 HG2 CG2 CB CA +2 6 HG1 CG1 CB CD +3 4 HD CD CG1 CB +VAL 5 +1 1 H N -C CA +1 5 HA CA N CB C +1 5 HB CB CA CG1 CG2 +3 4 HG1 CG1 CB CA +3 4 HG2 CG2 CB CA +ASN 4 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA CG +2 3 HD2 ND2 CG CB +GLN 5 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA CG +2 6 HG CG CB CD +2 3 HE2 NE2 CD CG +ARG 8 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA CG +2 6 HG CG CB CD +2 6 HD CD CG NE +1 1 HE NE CD CZ +2 3 HH1 NH1 CZ NE +2 3 HH2 NH2 CZ NE +HID 6 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA CG +1 1 HD1 ND1 CG CE1 +1 1 HE1 CE1 ND1 NE2 +1 1 HD2 CD2 CG NE2 +HIE 6 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA CG +1 1 HE1 CE1 ND1 NE2 +1 1 HE2 NE2 CE1 CD2 +1 1 HD2 CD2 CG NE2 +HIP 7 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA CG +1 1 HD1 ND1 CG CE1 +1 1 HE1 CE1 ND1 NE2 +1 1 HE2 NE2 CE1 CD2 +1 1 HD2 CD2 CG NE2 +TRP 9 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA CG +1 1 HD1 CD1 CG NE1 +1 1 HE1 NE1 CD1 CE2 +1 1 HZ2 CZ2 CE2 CH2 +1 1 HH2 CH2 CZ2 CZ3 +1 1 HZ3 CZ3 CH2 CE3 +1 1 HE3 CE3 CZ3 CD2 +PHE 8 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA CG +1 1 HD1 CD1 CG CE1 +1 1 HE1 CE1 CD1 CZ +1 1 HZ CZ CE1 CE2 +1 1 HE2 CE2 CZ CD2 +1 1 HD2 CD2 CG CE2 +TYR 8 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA CG +1 1 HD1 CD1 CG CE1 +1 1 HE1 CE1 CD1 CZ +1 2 HH OH CZ CE1 +1 1 HE2 CE2 CZ CD2 +1 1 HD2 CD2 CG CE2 +GLU 4 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA CG +2 6 HG CG CB CD +ASP 3 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA CG +LYS 7 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA CG +2 6 HG CG CB CD +2 6 HD CD CG CE +2 6 HE CE CD NZ +3 4 HZ NZ CE CD +ORN 6 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA CG +2 6 HG CG CB CD +2 6 HD CD CG NE +3 4 HE NE CD CG +DAB 5 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA CG +2 6 HG CG CB ND +3 4 HD ND CG CB +LYN 7 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA CG +2 6 HG CG CB CD +2 6 HD CD CG CE +2 6 HE CE CD NZ +2 3 HZ NZ CE CD +PRO 4 +2 6 HD CD N CG +2 6 HG CG CD CB +2 6 HB CB CG CA +1 5 HA CA N CB C +HYP 5 +2 6 HD2 CD2 N CG +1 5 HG CG CD2 OD1 CB +1 2 HD1 OD1 CG CD2 +2 6 HB CB CG CA +1 5 HA CA N CB C +CYS 4 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA SG +1 2 HG SG CB CA +CYM 3 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA SG +CYX 3 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA SG +CYS2 3 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA SG +MET 5 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA CG +2 6 HG CG CB SD +3 4 HE CE SD CG +ASH 4 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA CG +1 2 HD2 OD2 CG CB +GLH 5 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA CG +2 6 HG CG CB CD +1 2 HE2 OE2 CD CG +CALA 4 +1 1 H N -C CA +1 5 HA CA N CB C +3 4 HB CB CA N +1 1 OC1 C CA OC2 +CGLY 3 +1 1 H N -C CA +2 6 HA CA N C +1 1 OC1 C CA OC2 +CSER 5 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA OG +1 2 HG OG CB CA +1 1 OC1 C CA OC2 +CTHR 6 +1 1 H N -C CA +1 5 HA CA N CB C +1 5 HB CB CA CG2 OG1 +3 4 HG2 CG2 CB CA +1 2 HG1 OG1 CB CA +1 1 OC1 C CA OC2 +CLEU 7 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA CG +1 5 HG CG CB CD1 CD2 +3 4 HD1 CD1 CG CB +3 4 HD2 CD2 CG CB +1 1 OC1 C CA OC2 +CILE 7 +1 1 H N -C CA +1 5 HA CA N CB C +1 5 HB CB CA CG2 CG1 +3 4 HG2 CG2 CB CA +2 6 HG1 CG1 CB CD +3 4 HD CD CG1 CB +1 1 OC1 C CA OC2 +CVAL 6 +1 1 H N -C CA +1 5 HA CA N CB C +1 5 HB CB CA CG1 CG2 +3 4 HG1 CG1 CB CA +3 4 HG2 CG2 CB CA +1 1 OC1 C CA OC2 +CASN 5 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA CG +2 3 HD2 ND2 CG CB +1 1 OC1 C CA OC2 +CGLN 6 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA CG +2 6 HG CG CB CD +2 3 HE2 NE2 CD CG +1 1 OC1 C CA OC2 +CARG 9 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA CG +2 6 HG CG CB CD +2 6 HD CD CG NE +1 1 HE NE CD CZ +2 3 HH1 NH1 CZ NE +2 3 HH2 NH2 CZ NE +1 1 OC1 C CA OC2 +CHID 7 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA CG +1 1 HD1 ND1 CG CE1 +1 1 HE1 CE1 ND1 NE2 +1 1 HD2 CD2 CG NE2 +1 1 OC1 C CA OC2 +CHIE 7 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA CG +1 1 HE1 CE1 ND1 NE2 +1 1 HE2 NE2 CE1 CD2 +1 1 HD2 CD2 CG NE2 +1 1 OC1 C CA OC2 +CHIP 8 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA CG +1 1 HD1 ND1 CG CE1 +1 1 HE1 CE1 ND1 NE2 +1 1 HE2 NE2 CE1 CD2 +1 1 HD2 CD2 CG NE2 +1 1 OC1 C CA OC2 +CTRP 10 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA CG +1 1 HD1 CD1 CG NE1 +1 1 HE1 NE1 CD1 CE2 +1 1 HZ2 CZ2 CE2 CH2 +1 1 HH2 CH2 CZ2 CZ3 +1 1 HZ3 CZ3 CH2 CE3 +1 1 HE3 CE3 CZ3 CD2 +1 1 OC1 C CA OC2 +CPHE 9 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA CG +1 1 HD1 CD1 CG CE1 +1 1 HE1 CE1 CD1 CZ +1 1 HZ CZ CE1 CE2 +1 1 HE2 CE2 CZ CD2 +1 1 HD2 CD2 CG CE2 +1 1 OC1 C CA OC2 +CTYR 9 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA CG +1 1 HD1 CD1 CG CE1 +1 1 HE1 CE1 CD1 CZ +1 2 HH OH CZ CE1 +1 1 HE2 CE2 CZ CD2 +1 1 HD2 CD2 CG CE2 +1 1 OC1 C CA OC2 +CGLU 5 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA CG +2 6 HG CG CB CD +1 1 OC1 C CA OC2 +CASP 4 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA CG +1 1 OC1 C CA OC2 +CLYS 8 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA CG +2 6 HG CG CB CD +2 6 HD CD CG CE +2 6 HE CE CD NZ +3 4 HZ NZ CE CD +1 1 OC1 C CA OC2 +CPRO 5 +2 6 HD CD N CG +2 6 HG CG CD CB +2 6 HB CB CG CA +1 5 HA CA N CB C +1 1 OC1 C CA OC2 +CCYS 5 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA SG +1 2 HG SG CB CA +1 1 OC1 C CA OC2 +CCYX 4 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA SG +1 1 OC1 C CA OC2 +CMET 6 +1 1 H N -C CA +1 5 HA CA N CB C +2 6 HB CB CA CG +2 6 HG CG CB SD +3 4 HE CE SD CG +1 1 OC1 C CA OC2 +NALA 3 +3 4 H N CA CB +1 5 HA CA N CB C +3 4 HB CB CA N +NGLY 2 +3 4 H N CA C +2 6 HA CA N C +NSER 4 +3 4 H N CA CB +1 5 HA CA N CB C +2 6 HB CB CA OG +1 2 HG OG CB CA +NTHR 5 +3 4 H N CA CB +1 5 HA CA N CB C +1 5 HB CB CA CG2 OG1 +3 4 HG2 CG2 CB CA +1 2 HG1 OG1 CB CA +NLEU 6 +3 4 H N CA CB +1 5 HA CA N CB C +2 6 HB CB CA CG +1 5 HG CG CB CD1 CD2 +3 4 HD1 CD1 CG CB +3 4 HD2 CD2 CG CB +NILE 6 +3 4 H N CA CB +1 5 HA CA N CB C +1 5 HB CB CA CG2 CG1 +3 4 HG2 CG2 CB CA +2 6 HG1 CG1 CB CD +3 4 HD CD CG1 CB +NVAL 5 +3 4 H N CA CB +1 5 HA CA N CB C +1 5 HB CB CA CG1 CG2 +3 4 HG1 CG1 CB CA +3 4 HG2 CG2 CB CA +NASN 4 +3 4 H N CA CB +1 5 HA CA N CB C +2 6 HB CB CA CG +2 3 HD2 ND2 CG CB +NGLN 5 +3 4 H N CA CB +1 5 HA CA N CB C +2 6 HB CB CA CG +2 6 HG CG CB CD +2 3 HE2 NE2 CD CG +NARG 8 +3 4 H N CA CB +1 5 HA CA N CB C +2 6 HB CB CA CG +2 6 HG CG CB CD +2 6 HD CD CG NE +1 1 HE NE CD CZ +2 3 HH1 NH1 CZ NE +2 3 HH2 NH2 CZ NE +NHID 6 +3 4 H N CA CB +1 5 HA CA N CB C +2 6 HB CB CA CG +1 1 HD1 ND1 CG CE1 +1 1 HE1 CE1 ND1 NE2 +1 1 HD2 CD2 CG NE2 +NHIE 6 +3 4 H N CA CB +1 5 HA CA N CB C +2 6 HB CB CA CG +1 1 HE1 CE1 ND1 NE2 +1 1 HE2 NE2 CE1 CD2 +1 1 HD2 CD2 CG NE2 +NHIP 7 +3 4 H N CA CB +1 5 HA CA N CB C +2 6 HB CB CA CG +1 1 HD1 ND1 CG CE1 +1 1 HE1 CE1 ND1 NE2 +1 1 HE2 NE2 CE1 CD2 +1 1 HD2 CD2 CG NE2 +NTRP 9 +3 4 H N CA CB +1 5 HA CA N CB C +2 6 HB CB CA CG +1 1 HD1 CD1 CG NE1 +1 1 HE1 NE1 CD1 CE2 +1 1 HZ2 CZ2 CE2 CH2 +1 1 HH2 CH2 CZ2 CZ3 +1 1 HZ3 CZ3 CH2 CE3 +1 1 HE3 CE3 CZ3 CD2 +NPHE 8 +3 4 H N CA CB +1 5 HA CA N CB C +2 6 HB CB CA CG +1 1 HD1 CD1 CG CE1 +1 1 HE1 CE1 CD1 CZ +1 1 HZ CZ CE1 CE2 +1 1 HE2 CE2 CZ CD2 +1 1 HD2 CD2 CG CE2 +NTYR 8 +3 4 H N CA CB +1 5 HA CA N CB C +2 6 HB CB CA CG +1 1 HD1 CD1 CG CE1 +1 1 HE1 CE1 CD1 CZ +1 2 HH OH CZ CE1 +1 1 HE2 CE2 CZ CD2 +1 1 HD2 CD2 CG CE2 +NGLU 4 +3 4 H N CA CB +1 5 HA CA N CB C +2 6 HB CB CA CG +2 6 HG CG CB CD +NASP 3 +3 4 H N CA CB +1 5 HA CA N CB C +2 6 HB CB CA CG +NLYS 7 +3 4 H N CA CB +1 5 HA CA N CB C +2 6 HB CB CA CG +2 6 HG CG CB CD +2 6 HD CD CG CE +2 6 HE CE CD NZ +3 4 HZ NZ CE CD +NPRO 5 +2 6 H N CA CD +2 6 HD CD N CG +2 6 HG CG CD CB +2 6 HB CB CG CA +1 5 HA CA N CB C +NCYS 4 +3 4 H N CA CB +1 5 HA CA N CB C +2 6 HB CB CA SG +1 2 HG SG CB CA +NCYX 3 +3 4 H N CA CB +1 5 HA CA N CB C +2 6 HB CB CA SG +NMET 5 +3 4 H N CA CB +1 5 HA CA N CB C +2 6 HB CB CA CG +2 6 HG CG CB SD +3 4 HE CE SD CG diff --git a/pmx/data/mutff45/amber14sb_OL15.ff/aminoacids.n.tdb b/pmx/data/mutff45/amber14sb_OL15.ff/aminoacids.n.tdb new file mode 100644 index 00000000..50d0742f --- /dev/null +++ b/pmx/data/mutff45/amber14sb_OL15.ff/aminoacids.n.tdb @@ -0,0 +1,3 @@ +; empty + + diff --git a/pmx/data/mutff45/amber14sb_OL15.ff/aminoacids.r2b b/pmx/data/mutff45/amber14sb_OL15.ff/aminoacids.r2b new file mode 100644 index 00000000..bba55ba8 --- /dev/null +++ b/pmx/data/mutff45/amber14sb_OL15.ff/aminoacids.r2b @@ -0,0 +1,31 @@ +; rtp residue to rtp building block table +;GMX Force-field +; main N-ter C-ter 2-ter +ALA ALA NALA CALA - +ARG ARG NARG CARG - +ARGN - - - - +ASN ASN NASN CASN - +ASP ASP NASP CASP - +ASPH ASH - - - +CYS CYS NCYS CCYS - +CYS2 CYX NCYX CCYX - +GLN GLN NGLN CGLN - +QLN - - - - +GLU GLU NGLU CGLU - +GLY GLY NGLY CGLY - +GLUH GLH - - - +HISD HID NHID CHID - +HISE HIE NHIE CHIE - +HISH HIP NHIP CHIP - +ILE ILE NILE CILE - +LYSN LYN - - - +LYS LYS NLYS CLYS - +LEU LEU NLEU CLEU - +MET MET NMET CMET - +PHE PHE NPHE CPHE - +PRO PRO NPRO CPRO - +SER SER NSER CSER - +THR THR NTHR CTHR - +TRP TRP NTRP CTRP - +TYR TYR NTYR CTYR - +VAL VAL NVAL CVAL - diff --git a/pmx/data/mutff45/amber14sb_OL15.ff/aminoacids.rtp b/pmx/data/mutff45/amber14sb_OL15.ff/aminoacids.rtp new file mode 100644 index 00000000..81cddfbb --- /dev/null +++ b/pmx/data/mutff45/amber14sb_OL15.ff/aminoacids.rtp @@ -0,0 +1,3401 @@ +[ bondedtypes ] +; Col 1: Type of bond +; Col 2: Type of angles +; Col 3: Type of proper dihedrals +; Col 4: Type of improper dihedrals +; Col 5: Generate all dihedrals if 1, only heavy atoms of 0. +; Col 6: Number of excluded neighbors for nonbonded interactions +; Col 7: Generate 1,4 interactions between pairs of hydrogens if 1 +; Col 8: Remove impropers over the same bond as a proper if it is 1 +; bonds angles dihedrals impropers all_dihedrals nrexcl HH14 RemoveDih + 1 1 9 4 1 3 1 0 + +; now: water, ions, urea, terminal caps, AA's and terminal AA's + +; tip3p +[ HOH ] + [ atoms ] + OW OW -0.834 0 + HW1 HW 0.417 0 + HW2 HW 0.417 0 + [ bonds ] + OW HW1 + OW HW2 + +; tip4p +[ HO4 ] + [ atoms ] + OW OW_tip4p 0.00 0 + HW1 HW 0.52 0 + HW2 HW 0.52 0 + MW MW -1.04 0 + [ bonds ] + OW HW1 + OW HW2 + +[ IB+ ] ; big positive ion + [ atoms ] + IB IB 1.00000 1 + +[ CA ] + [ atoms ] + CA C0 2.00000 1 + +[ CL ] + [ atoms ] + CL Cl -1.00000 1 + +[ NA ] + [ atoms ] + NA Na 1.00000 1 + +[ MG ] + [ atoms ] + MG MG 2.00000 1 + +[ K ] + [ atoms ] + K K 1.00000 1 + +[ RB ] + [ atoms ] + RB Rb 1.00000 1 + +[ CS ] + [ atoms ] + CS Cs 1.00000 1 + +[ LI ] + [ atoms ] + LI Li 1.00000 1 + +[ ZN ] + [ atoms ] + ZN Zn 2.00000 1 + +[ URE ] ; urea added in by EJS, resp charges by Jim Caldwell + [ atoms ] + C C 0.880229 1 + O O -0.613359 2 + N1 N -0.923545 3 + H11 H 0.395055 4 + H12 H 0.395055 5 + N2 N -0.923545 6 + H21 H 0.395055 7 + H22 H 0.395055 8 + [ bonds ] + C N1 + C N2 + C O + N1 H11 + N1 H12 + N2 H21 + N2 H22 + [ impropers ] + N1 N2 C O + C H11 N1 H12 + C H21 N2 H22 + +[ ACE ] + [ atoms ] + HH31 HC 0.11230 1 + CH3 CT -0.36620 2 + HH32 HC 0.11230 3 + HH33 HC 0.11230 4 + C C 0.59720 5 + O O -0.56790 6 + [ bonds ] + HH31 CH3 + CH3 HH32 + CH3 HH33 + CH3 C + C O + [ impropers ] + CH3 +N C O + +[ NME ] + [ atoms ] + N N -0.41570 1 + H H 0.27190 2 + CH3 CT -0.14900 3 + HH31 H1 0.09760 4 + HH32 H1 0.09760 5 + HH33 H1 0.09760 6 + [ bonds ] + N H + N CH3 + CH3 HH31 + CH3 HH32 + CH3 HH33 + -C N + [ impropers ] + -C CH3 N H + +[ NHE ] + [ atoms ] + N N -0.46300 1 + H1 H 0.23150 2 + H2 H 0.23150 3 + [ bonds ] + N H1 + N H2 + -C N + [ impropers ] + -C H1 N H2 + +[ NH2 ] + [ atoms ] + N N -0.46300 1 + H1 H 0.23150 2 + H2 H 0.23150 3 + [ bonds ] + N H1 + N H2 + -C N + [ impropers ] + -C H1 N H2 + +; Next are non-terminal AA's + +[ ALA ] + [ atoms ] + N N -0.41570 1 + H H 0.27190 2 + CA CX 0.03370 3 + HA H1 0.08230 4 + CB CT -0.18250 5 + HB1 HC 0.06030 6 + HB2 HC 0.06030 7 + HB3 HC 0.06030 8 + C C 0.59730 9 + O O -0.56790 10 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB HB3 + C O + -C N + [ impropers ] + -C CA N H + CA +N C O + +[ GLY ] + [ atoms ] + N N -0.41570 1 + H H 0.27190 2 + CA CX -0.02520 3 + HA1 H1 0.06980 4 + HA2 H1 0.06980 5 + C C 0.59730 6 + O O -0.56790 7 + [ bonds ] + N H + N CA + CA HA1 + CA HA2 + CA C + C O + -C N + [ impropers ] + -C CA N H + CA +N C O + +[ SER ] + [ atoms ] + N N -0.41570 1 + H H 0.27190 2 + CA CX -0.02490 3 + HA H1 0.08430 4 + CB 2C 0.21170 5 + HB1 H1 0.03520 6 + HB2 H1 0.03520 7 + OG OH -0.65460 8 + HG HO 0.42750 9 + C C 0.59730 10 + O O -0.56790 11 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB OG + OG HG + C O + -C N + [ impropers ] + -C CA N H + CA +N C O + +[ THR ] + [ atoms ] + N N -0.41570 1 + H H 0.27190 2 + CA CX -0.03890 3 + HA H1 0.10070 4 + CB 3C 0.36540 5 + HB H1 0.00430 6 + CG2 CT -0.24380 7 + HG21 HC 0.06420 8 + HG22 HC 0.06420 9 + HG23 HC 0.06420 10 + OG1 OH -0.67610 11 + HG1 HO 0.41020 12 + C C 0.59730 13 + O O -0.56790 14 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB + CB CG2 + CB OG1 + CG2 HG21 + CG2 HG22 + CG2 HG23 + OG1 HG1 + C O + -C N + [ impropers ] + -C CA N H + CA +N C O + +[ LEU ] + [ atoms ] + N N -0.41570 1 + H H 0.27190 2 + CA CX -0.05180 3 + HA H1 0.09220 4 + CB 2C -0.11020 5 + HB1 HC 0.04570 6 + HB2 HC 0.04570 7 + CG 3C 0.35310 8 + HG HC -0.03610 9 + CD1 CT -0.41210 10 + HD11 HC 0.10000 11 + HD12 HC 0.10000 12 + HD13 HC 0.10000 13 + CD2 CT -0.41210 14 + HD21 HC 0.10000 15 + HD22 HC 0.10000 16 + HD23 HC 0.10000 17 + C C 0.59730 18 + O O -0.56790 19 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG HG + CG CD1 + CG CD2 + CD1 HD11 + CD1 HD12 + CD1 HD13 + CD2 HD21 + CD2 HD22 + CD2 HD23 + C O + -C N + [ impropers ] + -C CA N H + CA +N C O + +[ ILE ] + [ atoms ] + N N -0.41570 1 + H H 0.27190 2 + CA CX -0.05970 3 + HA H1 0.08690 4 + CB 3C 0.13030 5 + HB HC 0.01870 6 + CG2 CT -0.32040 7 + HG21 HC 0.08820 8 + HG22 HC 0.08820 9 + HG23 HC 0.08820 10 + CG1 2C -0.04300 11 + HG11 HC 0.02360 12 + HG12 HC 0.02360 13 + CD CT -0.06600 14 + HD1 HC 0.01860 15 + HD2 HC 0.01860 16 + HD3 HC 0.01860 17 + C C 0.59730 18 + O O -0.56790 19 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB + CB CG2 + CB CG1 + CG2 HG21 + CG2 HG22 + CG2 HG23 + CG1 HG11 + CG1 HG12 + CG1 CD + CD HD1 + CD HD2 + CD HD3 + C O + -C N + [ impropers ] + -C CA N H + CA +N C O + +[ VAL ] + [ atoms ] + N N -0.41570 1 + H H 0.27190 2 + CA CX -0.08750 3 + HA H1 0.09690 4 + CB 3C 0.29850 5 + HB HC -0.02970 6 + CG1 CT -0.31920 7 + HG11 HC 0.07910 8 + HG12 HC 0.07910 9 + HG13 HC 0.07910 10 + CG2 CT -0.31920 11 + HG21 HC 0.07910 12 + HG22 HC 0.07910 13 + HG23 HC 0.07910 14 + C C 0.59730 15 + O O -0.56790 16 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB + CB CG1 + CB CG2 + CG1 HG11 + CG1 HG12 + CG1 HG13 + CG2 HG21 + CG2 HG22 + CG2 HG23 + C O + -C N + [ impropers ] + -C CA N H + CA +N C O + +[ ASN ] + [ atoms ] + N N -0.41570 1 + H H 0.27190 2 + CA CX 0.01430 3 + HA H1 0.10480 4 + CB 2C -0.20410 5 + HB1 HC 0.07970 6 + HB2 HC 0.07970 7 + CG C 0.71300 8 + OD1 O -0.59310 9 + ND2 N -0.91910 10 + HD21 H 0.41960 11 + HD22 H 0.41960 12 + C C 0.59730 13 + O O -0.56790 14 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG OD1 + CG ND2 + ND2 HD21 + ND2 HD22 + C O + -C N + [ impropers ] + -C CA N H + CA +N C O + CB ND2 CG OD1 + CG HD21 ND2 HD22 + +[ GLN ] + [ atoms ] + N N -0.41570 1 + H H 0.27190 2 + CA CX -0.00310 3 + HA H1 0.08500 4 + CB 2C -0.00360 5 + HB1 HC 0.01710 6 + HB2 HC 0.01710 7 + CG 2C -0.06450 8 + HG1 HC 0.03520 9 + HG2 HC 0.03520 10 + CD C 0.69510 11 + OE1 O -0.60860 12 + NE2 N -0.94070 13 + HE21 H 0.42510 14 + HE22 H 0.42510 15 + C C 0.59730 16 + O O -0.56790 17 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG HG1 + CG HG2 + CG CD + CD OE1 + CD NE2 + NE2 HE21 + NE2 HE22 + C O + -C N + [ impropers ] + -C CA N H + CA +N C O + CG NE2 CD OE1 + CD HE21 NE2 HE22 + +[ ARG ] + [ atoms ] + N N -0.34790 1 + H H 0.27470 2 + CA CX -0.26370 3 + HA H1 0.15600 4 + CB C8 -0.00070 5 + HB1 HC 0.03270 6 + HB2 HC 0.03270 7 + CG C8 0.03900 8 + HG1 HC 0.02850 9 + HG2 HC 0.02850 10 + CD C8 0.04860 11 + HD1 H1 0.06870 12 + HD2 H1 0.06870 13 + NE N2 -0.52950 14 + HE H 0.34560 15 + CZ CA 0.80760 16 + NH1 N2 -0.86270 17 + HH11 H 0.44780 18 + HH12 H 0.44780 19 + NH2 N2 -0.86270 20 + HH21 H 0.44780 21 + HH22 H 0.44780 22 + C C 0.73410 23 + O O -0.58940 24 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG HG1 + CG HG2 + CG CD + CD HD1 + CD HD2 + CD NE + NE HE + NE CZ + CZ NH1 + CZ NH2 + NH1 HH11 + NH1 HH12 + NH2 HH21 + NH2 HH22 + C O + -C N + [ impropers ] + -C CA N H + CA +N C O + NE NH1 CZ NH2 + CD CZ NE HE + CZ HH11 NH1 HH12 + CZ HH21 NH2 HH22 + +[ HID ] + [ atoms ] + N N -0.41570 1 + H H 0.27190 2 + CA CX 0.01880 3 + HA H1 0.08810 4 + CB CT -0.04620 5 + HB1 HC 0.04020 6 + HB2 HC 0.04020 7 + CG CC -0.02660 8 + ND1 NA -0.38110 9 + HD1 H 0.36490 10 + CE1 CR 0.20570 11 + HE1 H5 0.13920 12 + NE2 NB -0.57270 13 + CD2 CV 0.12920 14 + HD2 H4 0.11470 15 + C C 0.59730 16 + O O -0.56790 17 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG ND1 + CG CD2 + ND1 HD1 + ND1 CE1 + CE1 HE1 + CE1 NE2 + NE2 CD2 + CD2 HD2 + C O + -C N + [ impropers ] + -C CA N H + CA +N C O + CG CE1 ND1 HD1 + CG NE2 CD2 HD2 + ND1 NE2 CE1 HE1 + ND1 CD2 CG CB + +[ HIE ] + [ atoms ] + N N -0.41570 1 + H H 0.27190 2 + CA CX -0.05810 3 + HA H1 0.13600 4 + CB CT -0.00740 5 + HB1 HC 0.03670 6 + HB2 HC 0.03670 7 + CG CC 0.18680 8 + ND1 NB -0.54320 9 + CE1 CR 0.16350 10 + HE1 H5 0.14350 11 + NE2 NA -0.27950 12 + HE2 H 0.33390 13 + CD2 CW -0.22070 14 + HD2 H4 0.18620 15 + C C 0.59730 16 + O O -0.56790 17 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB2 + CB HB1 + CB CG + CG ND1 + CG CD2 + ND1 CE1 + CE1 HE1 + CE1 NE2 + NE2 HE2 + NE2 CD2 + CD2 HD2 + C O + -C N + [ impropers ] + -C CA N H + CA +N C O + CE1 CD2 NE2 HE2 + CG NE2 CD2 HD2 + ND1 NE2 CE1 HE1 + ND1 CD2 CG CB + +[ HIP ] + [ atoms ] + N N -0.34790 1 + H H 0.27470 2 + CA CX -0.13540 3 + HA H1 0.12120 4 + CB CT -0.04140 5 + HB1 HC 0.08100 6 + HB2 HC 0.08100 7 + CG CC -0.00120 8 + ND1 NA -0.15130 9 + HD1 H 0.38660 10 + CE1 CR -0.01700 11 + HE1 H5 0.26810 12 + NE2 NA -0.17180 13 + HE2 H 0.39110 14 + CD2 CW -0.11410 15 + HD2 H4 0.23170 16 + C C 0.73410 17 + O O -0.58940 18 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG ND1 + CG CD2 + ND1 HD1 + ND1 CE1 + CE1 HE1 + CE1 NE2 + NE2 HE2 + NE2 CD2 + CD2 HD2 + C O + -C N + [ impropers ] + -C CA N H + CA +N C O + CG CE1 ND1 HD1 + CE1 CD2 NE2 HE2 + CG NE2 CD2 HD2 + ND1 NE2 CE1 HE1 + ND1 CD2 CG CB + +[ TRP ] + [ atoms ] + N N -0.41570 1 + H H 0.27190 2 + CA CX -0.02750 3 + HA H1 0.11230 4 + CB CT -0.00500 5 + HB1 HC 0.03390 6 + HB2 HC 0.03390 7 + CG C* -0.14150 8 + CD1 CW -0.16380 9 + HD1 H4 0.20620 10 + NE1 NA -0.34180 11 + HE1 H 0.34120 12 + CE2 CN 0.13800 13 + CZ2 CA -0.26010 14 + HZ2 HA 0.15720 15 + CH2 CA -0.11340 16 + HH2 HA 0.14170 17 + CZ3 CA -0.19720 18 + HZ3 HA 0.14470 19 + CE3 CA -0.23870 20 + HE3 HA 0.17000 21 + CD2 CB 0.12430 22 + C C 0.59730 23 + O O -0.56790 24 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG CD1 + CG CD2 + CD1 HD1 + CD1 NE1 + NE1 HE1 + NE1 CE2 + CE2 CZ2 + CE2 CD2 + CZ2 HZ2 + CZ2 CH2 + CH2 HH2 + CH2 CZ3 + CZ3 HZ3 + CZ3 CE3 + CE3 HE3 + CE3 CD2 + C O + -C N + [ impropers ] + -C CA N H + CA +N C O + CD1 CE2 NE1 HE1 + CE2 CH2 CZ2 HZ2 + CZ2 CZ3 CH2 HH2 + CH2 CE3 CZ3 HZ3 + CZ3 CD2 CE3 HE3 + CG NE1 CD1 HD1 + CD1 CG CB CD2 + +[ PHE ] + [ atoms ] + N N -0.41570 1 + H H 0.27190 2 + CA CX -0.00240 3 + HA H1 0.09780 4 + CB CT -0.03430 5 + HB1 HC 0.02950 6 + HB2 HC 0.02950 7 + CG CA 0.01180 8 + CD1 CA -0.12560 9 + HD1 HA 0.13300 10 + CE1 CA -0.17040 11 + HE1 HA 0.14300 12 + CZ CA -0.10720 13 + HZ HA 0.12970 14 + CE2 CA -0.17040 15 + HE2 HA 0.14300 16 + CD2 CA -0.12560 17 + HD2 HA 0.13300 18 + C C 0.59730 19 + O O -0.56790 20 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG CD1 + CG CD2 + CD1 HD1 + CD1 CE1 + CE1 HE1 + CE1 CZ + CZ HZ + CZ CE2 + CE2 HE2 + CE2 CD2 + CD2 HD2 + C O + -C N + [ impropers ] + -C CA N H + CA +N C O + CG CE2 CD2 HD2 + CZ CD2 CE2 HE2 + CE1 CE2 CZ HZ + CD1 CZ CE1 HE1 + CG CE1 CD1 HD1 + CD1 CD2 CG CB + +[ TYR ] + [ atoms ] + N N -0.41570 1 + H H 0.27190 2 + CA CX -0.00140 3 + HA H1 0.08760 4 + CB CT -0.01520 5 + HB1 HC 0.02950 6 + HB2 HC 0.02950 7 + CG CA -0.00110 8 + CD1 CA -0.19060 9 + HD1 HA 0.16990 10 + CE1 CA -0.23410 11 + HE1 HA 0.16560 12 + CZ C 0.32260 13 + OH OH -0.55790 14 + HH HO 0.39920 15 + CE2 CA -0.23410 16 + HE2 HA 0.16560 17 + CD2 CA -0.19060 18 + HD2 HA 0.16990 19 + C C 0.59730 20 + O O -0.56790 21 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG CD1 + CG CD2 + CD1 HD1 + CD1 CE1 + CE1 HE1 + CE1 CZ + CZ OH + CZ CE2 + OH HH + CE2 HE2 + CE2 CD2 + CD2 HD2 + C O + -C N + [ impropers ] + -C CA N H + CA +N C O + CG CE2 CD2 HD2 + CZ CD2 CE2 HE2 + CD1 CZ CE1 HE1 + CG CE1 CD1 HD1 + CD1 CD2 CG CB + CE1 CE2 CZ OH + +[ GLU ] + [ atoms ] + N N -0.51630 1 + H H 0.29360 2 + CA CX 0.03970 3 + HA H1 0.11050 4 + CB 2C 0.05600 5 + HB1 HC -0.01730 6 + HB2 HC -0.01730 7 + CG 2C 0.01360 8 + HG1 HC -0.04250 9 + HG2 HC -0.04250 10 + CD CO 0.80540 11 + OE1 O2 -0.81880 12 + OE2 O2 -0.81880 13 + C C 0.53660 14 + O O -0.58190 15 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG HG1 + CG HG2 + CG CD + CD OE1 + CD OE2 + C O + -C N + [ impropers ] + -C CA N H + CA +N C O + CG OE1 CD OE2 + +[ ASP ] + [ atoms ] + N N -0.51630 1 + H H 0.29360 2 + CA CX 0.03810 3 + HA H1 0.08800 4 + CB 2C -0.03030 5 + HB1 HC -0.01220 6 + HB2 HC -0.01220 7 + CG CO 0.79940 8 + OD1 O2 -0.80140 9 + OD2 O2 -0.80140 10 + C C 0.53660 11 + O O -0.58190 12 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG OD1 + CG OD2 + C O + -C N + [ impropers ] + -C CA N H + CA +N C O + CB OD1 CG OD2 + +[ LYS ] + [ atoms ] + N N -0.34790 1 + H H 0.27470 2 + CA CX -0.24000 3 + HA H1 0.14260 4 + CB C8 -0.00940 5 + HB1 HC 0.03620 6 + HB2 HC 0.03620 7 + CG C8 0.01870 8 + HG1 HC 0.01030 9 + HG2 HC 0.01030 10 + CD C8 -0.04790 11 + HD1 HC 0.06210 12 + HD2 HC 0.06210 13 + CE C8 -0.01430 14 + HE1 HP 0.11350 15 + HE2 HP 0.11350 16 + NZ N3 -0.38540 17 + HZ1 H 0.34000 18 + HZ2 H 0.34000 19 + HZ3 H 0.34000 20 + C C 0.73410 21 + O O -0.58940 22 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG HG1 + CG HG2 + CG CD + CD HD1 + CD HD2 + CD CE + CE HE1 + CE HE2 + CE NZ + NZ HZ1 + NZ HZ2 + NZ HZ3 + C O + -C N + [ impropers ] + -C CA N H + CA +N C O + +[ ORN ] ; charges taken from amber99.prm of tinker 4.0 + [ atoms ] + N N -0.34790 1 + H H 0.27470 2 + CA CT -0.24000 3 + HA H1 0.14260 4 + CB CT 0.00990 5 + HB1 HC 0.03620 6 + HB2 HC 0.03620 7 + CG CT -0.02790 8 + HG1 HC 0.06210 9 + HG2 HC 0.06210 10 + CD CT -0.01430 11 + HD1 HP 0.11350 12 + HD2 HP 0.11350 13 + NE N3 -0.38540 14 + HE1 H 0.34000 15 + HE2 H 0.34000 16 + HE3 H 0.34000 17 + C C 0.73410 18 + O O -0.58940 19 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG HG1 + CG HG2 + CG CD + CD HD1 + CD HD2 + CD NE + NE HE1 + NE HE2 + NE HE3 + C O + -C N + [ impropers ] + -C CA N H + CA +N C O + +[ DAB ] ; sidechain charges fit to maintain heavy atom charge group trend LYS -> ORN -> DAB + [ atoms ] + N N -0.34790 1 + H H 0.27470 2 + CA CT -0.24000 3 + HA H1 0.14260 4 + CB CT 0.02920 5 + HB1 HC 0.07470 6 + HB2 HC 0.07470 7 + CG CT -0.01430 8 + HG1 HP 0.11350 9 + HG2 HP 0.11350 10 + ND N3 -0.38540 11 + HD1 H 0.34000 12 + HD2 H 0.34000 13 + HD3 H 0.34000 14 + C C 0.73410 15 + O O -0.58940 16 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG HG1 + CG HG2 + CG ND + ND HD1 + ND HD2 + ND HD3 + C O + -C N + [ impropers ] + -C CA N H + CA +N C O + +[ LYN ] + [ atoms ] + N N -0.41570 1 + H H 0.27190 2 + CA CX -0.07206 3 + HA H1 0.09940 4 + CB CT -0.04845 5 + HB1 HC 0.03400 6 + HB2 HC 0.03400 7 + CG CT 0.06612 8 + HG1 HC 0.01041 9 + HG2 HC 0.01041 10 + CD CT -0.03768 11 + HD1 HC 0.01155 12 + HD2 HC 0.01155 13 + CE CT 0.32604 14 + HE1 HP -0.03358 15 + HE2 HP -0.03358 16 + NZ N3 -1.03581 17 + HZ1 H 0.38604 18 + HZ2 H 0.38604 19 + C C 0.59730 20 + O O -0.56790 21 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG HG1 + CG HG2 + CG CD + CD HD1 + CD HD2 + CD CE + CE HE1 + CE HE2 + CE NZ + NZ HZ1 + NZ HZ2 + C O + -C N + [ impropers ] + -C CA N H + CA +N C O + +[ PRO ] + [ atoms ] + N N -0.25480 1 + CD CT 0.01920 2 + HD1 H1 0.03910 3 + HD2 H1 0.03910 4 + CG CT 0.01890 5 + HG1 HC 0.02130 6 + HG2 HC 0.02130 7 + CB CT -0.00700 8 + HB1 HC 0.02530 9 + HB2 HC 0.02530 10 + CA CX -0.02660 11 + HA H1 0.06410 12 + C C 0.58960 13 + O O -0.57480 14 + [ bonds ] + N CD + N CA + CD HD1 + CD HD2 + CD CG + CG HG1 + CG HG2 + CG CB + CB HB1 + CB HB2 + CB CA + CA HA + CA C + C O + -C N + [ impropers ] + CA +N C O + -C CD N CA + +[ HYP ] ; S Park, R J Radmer, T E Klein & V S Pande (submitted). + [ atoms ] + N N -0.25480 1 + CD2 CT 0.05950 2 + HD21 H1 0.07000 3 + HD22 H1 0.07000 4 + CG CT 0.04000 5 + HG H1 0.04160 6 + OD1 OH -0.61340 7 + HD1 HO 0.38510 8 + CB CT 0.02030 9 + HB1 HC 0.04260 10 + HB2 HC 0.04260 11 + CA CX 0.00470 12 + HA H1 0.07700 13 + C C 0.58960 14 + O O -0.57480 15 + [ bonds ] + N CD2 + N CA + CD2 HD21 + CD2 HD22 + CD2 CG + CG HG + CG OD1 + CG CB + OD1 HD1 + CB HB1 + CB HB2 + CB CA + CA HA + CA C + C O + -C N + [ impropers ] + CA +N C O + -C CD2 N CA + +[ CYS ] + [ atoms ] + N N -0.41570 1 + H H 0.27190 2 + CA CX 0.02130 3 + HA H1 0.11240 4 + CB 2C -0.12310 5 + HB1 H1 0.11120 6 + HB2 H1 0.11120 7 + SG SH -0.31190 8 + HG HS 0.19330 9 + C C 0.59730 10 + O O -0.56790 11 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB SG + SG HG + C O + -C N + [ impropers ] + -C CA N H + CA +N C O + +[ CYM ] + [ atoms ] + N N -0.41570 1 + H H 0.27190 2 + CA CX -0.03510 3 + HA H1 0.05080 4 + CB CT -0.24130 5 + HB1 H1 0.11220 6 + HB2 H1 0.11220 7 + SG SH -0.88440 8 + C C 0.59730 9 + O O -0.56790 10 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB SG + C O + -C N + [ impropers ] + -C CA N H + CA +N C O + +[ CYX ] + [ atoms ] + N N -0.41570 1 + H H 0.27190 2 + CA CX 0.04290 3 + HA H1 0.07660 4 + CB 2C -0.07900 5 + HB1 H1 0.09100 6 + HB2 H1 0.09100 7 + SG S -0.10810 8 + C C 0.59730 9 + O O -0.56790 10 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB SG + C O + -C N + [ impropers ] + -C CA N H + CA +N C O + +[ MET ] + [ atoms ] + N N -0.41570 1 + H H 0.27190 2 + CA CX -0.02370 3 + HA H1 0.08800 4 + CB 2C 0.03420 5 + HB1 HC 0.02410 6 + HB2 HC 0.02410 7 + CG 2C 0.00180 8 + HG1 H1 0.04400 9 + HG2 H1 0.04400 10 + SD S -0.27370 11 + CE CT -0.05360 12 + HE1 H1 0.06840 13 + HE2 H1 0.06840 14 + HE3 H1 0.06840 15 + C C 0.59730 16 + O O -0.56790 17 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG HG1 + CG HG2 + CG SD + SD CE + CE HE1 + CE HE2 + CE HE3 + C O + -C N + [ impropers ] + -C CA N H + CA +N C O + +; non-terminal acidic AA's + +[ ASH ] + [ atoms ] + N N -0.41570 1 + H H 0.27190 2 + CA CX 0.03410 3 + HA H1 0.08640 4 + CB 2C -0.03160 5 + HB1 HC 0.04880 6 + HB2 HC 0.04880 7 + CG C 0.64620 8 + OD1 O -0.55540 9 + OD2 OH -0.63760 10 + HD2 HO 0.47470 11 + C C 0.59730 12 + O O -0.56790 13 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG OD1 + CG OD2 + OD2 HD2 + C O + -C N + [ impropers ] + -C CA N H + CA +N C O + CB OD1 CG OD2 + + +[ GLH ] + [ atoms ] + N N -0.41570 1 + H H 0.27190 2 + CA CX 0.01450 3 + HA H1 0.07790 4 + CB 2C -0.00710 5 + HB1 HC 0.02560 6 + HB2 HC 0.02560 7 + CG 2C -0.01740 8 + HG1 HC 0.04300 9 + HG2 HC 0.04300 10 + CD C 0.68010 11 + OE1 O -0.58380 12 + OE2 OH -0.65110 13 + HE2 HO 0.46410 14 + C C 0.59730 15 + O O -0.56790 16 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG HG1 + CG HG2 + CG CD + CD OE1 + CD OE2 + OE2 HE2 + C O + -C N + [ impropers ] + -C CA N H + CA +N C O + CG OE1 CD OE2 + +; C-terminal AA's + +[ CALA ] + [ atoms ] + N N -0.38210 1 + H H 0.26810 2 + CA CX -0.17470 3 + HA H1 0.10670 4 + CB CT -0.20930 5 + HB1 HC 0.07640 6 + HB2 HC 0.07640 7 + HB3 HC 0.07640 8 + C C 0.77310 9 + OC1 O2 -0.80550 10 + OC2 O2 -0.80550 11 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB HB3 + C OC1 + C OC2 + -C N + [ impropers ] + -C CA N H + CA OC1 C OC2 + +[ CGLY ] + [ atoms ] + N N -0.38210 1 + H H 0.26810 2 + CA CX -0.24930 3 + HA1 H1 0.10560 4 + HA2 H1 0.10560 5 + C C 0.72310 6 + OC1 O2 -0.78550 7 + OC2 O2 -0.78550 8 + [ bonds ] + N H + N CA + CA HA1 + CA HA2 + CA C + C OC1 + C OC2 + -C N + [ impropers ] + -C CA N H + CA OC1 C OC2 + +[ CSER ] + [ atoms ] + N N -0.38210 1 + H H 0.26810 2 + CA CX -0.27220 3 + HA H1 0.13040 4 + CB 2C 0.11230 5 + HB1 H1 0.08130 6 + HB2 H1 0.08130 7 + OG OH -0.65140 8 + HG HO 0.44740 9 + C C 0.81130 10 + OC1 O2 -0.81320 11 + OC2 O2 -0.81320 12 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB OG + OG HG + C OC1 + C OC2 + -C N + [ impropers ] + -C CA N H + CA OC1 C OC2 + +[ CTHR ] + [ atoms ] + N N -0.38210 1 + H H 0.26810 2 + CA CX -0.24200 3 + HA H1 0.12070 4 + CB 3C 0.30250 5 + HB H1 0.00780 6 + CG2 CT -0.18530 7 + HG21 HC 0.05860 8 + HG22 HC 0.05860 9 + HG23 HC 0.05860 10 + OG1 OH -0.64960 11 + HG1 HO 0.41190 12 + C C 0.78100 13 + OC1 O2 -0.80440 14 + OC2 O2 -0.80440 15 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB + CB CG2 + CB OG1 + CG2 HG21 + CG2 HG22 + CG2 HG23 + OG1 HG1 + C OC1 + C OC2 + -C N + [ impropers ] + -C CA N H + CA OC1 C OC2 + +[ CLEU ] + [ atoms ] + N N -0.38210 1 + H H 0.26810 2 + CA CX -0.28470 3 + HA H1 0.13460 4 + CB 2C -0.24690 5 + HB1 HC 0.09740 6 + HB2 HC 0.09740 7 + CG 3C 0.37060 8 + HG HC -0.03740 9 + CD1 CT -0.41630 10 + HD11 HC 0.10380 11 + HD12 HC 0.10380 12 + HD13 HC 0.10380 13 + CD2 CT -0.41630 14 + HD21 HC 0.10380 15 + HD22 HC 0.10380 16 + HD23 HC 0.10380 17 + C C 0.83260 18 + OC1 O2 -0.81990 19 + OC2 O2 -0.81990 20 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG HG + CG CD1 + CG CD2 + CD1 HD11 + CD1 HD12 + CD1 HD13 + CD2 HD21 + CD2 HD22 + CD2 HD23 + C OC1 + C OC2 + -C N + [ impropers ] + -C CA N H + CA OC1 C OC2 + +[ CILE ] + [ atoms ] + N N -0.38210 1 + H H 0.26810 2 + CA CX -0.31000 3 + HA H1 0.13750 4 + CB 3C 0.03630 5 + HB HC 0.07660 6 + CG2 CT -0.34980 7 + HG21 HC 0.10210 8 + HG22 HC 0.10210 9 + HG23 HC 0.10210 10 + CG1 2C -0.03230 11 + HG11 HC 0.03210 12 + HG12 HC 0.03210 13 + CD CT -0.06990 14 + HD1 HC 0.01960 15 + HD2 HC 0.01960 16 + HD3 HC 0.01960 17 + C C 0.83430 18 + OC1 O2 -0.81900 19 + OC2 O2 -0.81900 20 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB + CB CG2 + CB CG1 + CG2 HG21 + CG2 HG22 + CG2 HG23 + CG1 HG11 + CG1 HG12 + CG1 CD + CD HD1 + CD HD2 + CD HD3 + C OC1 + C OC2 + -C N + [ impropers ] + -C CA N H + CA OC1 C OC2 + +[ CVAL ] + [ atoms ] + N N -0.38210 1 + H H 0.26810 2 + CA CX -0.34380 3 + HA H1 0.14380 4 + CB 3C 0.19400 5 + HB HC 0.03080 6 + CG1 CT -0.30640 7 + HG11 HC 0.08360 8 + HG12 HC 0.08360 9 + HG13 HC 0.08360 10 + CG2 CT -0.30640 11 + HG21 HC 0.08360 12 + HG22 HC 0.08360 13 + HG23 HC 0.08360 14 + C C 0.83500 15 + OC1 O2 -0.81730 16 + OC2 O2 -0.81730 17 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB + CB CG1 + CB CG2 + CG1 HG11 + CG1 HG12 + CG1 HG13 + CG2 HG21 + CG2 HG22 + CG2 HG23 + C OC1 + C OC2 + -C N + [ impropers ] + -C CA N H + CA OC1 C OC2 + +[ CASN ] + [ atoms ] + N N -0.38210 1 + H H 0.26810 2 + CA CX -0.20800 3 + HA H1 0.13580 4 + CB 2C -0.22990 5 + HB1 HC 0.10230 6 + HB2 HC 0.10230 7 + CG C 0.71530 8 + OD1 O -0.60100 9 + ND2 N -0.90840 10 + HD21 H 0.41500 11 + HD22 H 0.41500 12 + C C 0.80500 13 + OC1 O2 -0.81470 14 + OC2 O2 -0.81470 15 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG OD1 + CG ND2 + ND2 HD21 + ND2 HD22 + C OC1 + C OC2 + -C N + [ impropers ] + -C CA N H + CA OC1 C OC2 + CB ND2 CG OD1 + CG HD21 ND2 HD22 + +[ CGLN ] + [ atoms ] + N N -0.38210 1 + H H 0.26810 2 + CA CX -0.22480 3 + HA H1 0.12320 4 + CB 2C -0.06640 5 + HB1 HC 0.04520 6 + HB2 HC 0.04520 7 + CG 2C -0.02100 8 + HG1 HC 0.02030 9 + HG2 HC 0.02030 10 + CD C 0.70930 11 + OE1 O -0.60980 12 + NE2 N -0.95740 13 + HE21 H 0.43040 14 + HE22 H 0.43040 15 + C C 0.77750 16 + OC1 O2 -0.80420 17 + OC2 O2 -0.80420 18 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG HG1 + CG HG2 + CG CD + CD OE1 + CD NE2 + NE2 HE21 + NE2 HE22 + C OC1 + C OC2 + -C N + [ impropers ] + -C CA N H + CA OC1 C OC2 + CG NE2 CD OE1 + CD HE21 NE2 HE22 + +[ CARG ] + [ atoms ] + N N -0.34810 1 + H H 0.27640 2 + CA CX -0.30680 3 + HA H1 0.14470 4 + CB C8 -0.03740 5 + HB1 HC 0.03710 6 + HB2 HC 0.03710 7 + CG C8 0.07440 8 + HG1 HC 0.01850 9 + HG2 HC 0.01850 10 + CD C8 0.11140 11 + HD1 H1 0.04680 12 + HD2 H1 0.04680 13 + NE N2 -0.55640 14 + HE H 0.34790 15 + CZ CA 0.83680 16 + NH1 N2 -0.87370 17 + HH11 H 0.44930 18 + HH12 H 0.44930 19 + NH2 N2 -0.87370 20 + HH21 H 0.44930 21 + HH22 H 0.44930 22 + C C 0.85570 23 + OC1 O2 -0.82660 24 + OC2 O2 -0.82660 25 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG HG1 + CG HG2 + CG CD + CD HD1 + CD HD2 + CD NE + NE HE + NE CZ + CZ NH1 + CZ NH2 + NH1 HH11 + NH1 HH12 + NH2 HH21 + NH2 HH22 + C OC1 + C OC2 + -C N + [ impropers ] + -C CA N H + CA OC1 C OC2 + NE NH1 CZ NH2 + CD CZ NE HE + CZ HH11 NH1 HH12 + CZ HH21 NH2 HH22 + +[ CHID ] + [ atoms ] + N N -0.38210 1 + H H 0.26810 2 + CA CX -0.17390 3 + HA H1 0.11000 4 + CB CT -0.10460 5 + HB1 HC 0.05650 6 + HB2 HC 0.05650 7 + CG CC 0.02930 8 + ND1 NA -0.38920 9 + HD1 H 0.37550 10 + CE1 CR 0.19250 11 + HE1 H5 0.14180 12 + NE2 NB -0.56290 13 + CD2 CV 0.10010 14 + HD2 H4 0.12410 15 + C C 0.76150 16 + OC1 O2 -0.80160 17 + OC2 O2 -0.80160 18 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG ND1 + CG CD2 + ND1 HD1 + ND1 CE1 + CE1 HE1 + CE1 NE2 + NE2 CD2 + CD2 HD2 + C OC1 + C OC2 + -C N + [ impropers ] + -C CA N H + CA OC1 C OC2 + CG CE1 ND1 HD1 + CG NE2 CD2 HD2 + ND1 NE2 CE1 HE1 + ND1 CD2 CG CB + +[ CHIE ] + [ atoms ] + N N -0.38210 1 + H H 0.26810 2 + CA CX -0.26990 3 + HA H1 0.16500 4 + CB CT -0.10680 5 + HB1 HC 0.06200 6 + HB2 HC 0.06200 7 + CG CC 0.27240 8 + ND1 NB -0.55170 9 + CE1 CR 0.15580 10 + HE1 H5 0.14480 11 + NE2 NA -0.26700 12 + HE2 H 0.33190 13 + CD2 CW -0.25880 14 + HD2 H4 0.19570 15 + C C 0.79160 16 + OC1 O2 -0.80650 17 + OC2 O2 -0.80650 18 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG ND1 + CG CD2 + ND1 CE1 + CE1 HE1 + CE1 NE2 + NE2 HE2 + NE2 CD2 + CD2 HD2 + C OC1 + C OC2 + -C N + [ impropers ] + -C CA N H + CA OC1 C OC2 + CE1 CD2 NE2 HE2 + CG NE2 CD2 HD2 + ND1 NE2 CE1 HE1 + ND1 CD2 CG CB + +[ CHIP ] + [ atoms ] + N N -0.34810 1 + H H 0.27640 2 + CA CX -0.14450 3 + HA H1 0.11150 4 + CB CT -0.08000 5 + HB1 HC 0.08680 6 + HB2 HC 0.08680 7 + CG CC 0.02980 8 + ND1 NA -0.15010 9 + HD1 H 0.38830 10 + CE1 CR -0.02510 11 + HE1 H5 0.26940 12 + NE2 NA -0.16830 13 + HE2 H 0.39130 14 + CD2 CW -0.12560 15 + HD2 H4 0.23360 16 + C C 0.80320 17 + OC1 O2 -0.81770 18 + OC2 O2 -0.81770 19 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG ND1 + CG CD2 + ND1 HD1 + ND1 CE1 + CE1 HE1 + CE1 NE2 + NE2 HE2 + NE2 CD2 + CD2 HD2 + C OC1 + C OC2 + -C N + [ impropers ] + -C CA N H + CA OC1 C OC2 + CG CE1 ND1 HD1 + CE1 CD2 NE2 HE2 + CG NE2 CD2 HD2 + ND1 NE2 CE1 HE1 + ND1 CD2 CG CB + +[ CTRP ] + [ atoms ] + N N -0.38210 1 + H H 0.26810 2 + CA CX -0.20840 3 + HA H1 0.12720 4 + CB CT -0.07420 5 + HB1 HC 0.04970 6 + HB2 HC 0.04970 7 + CG C* -0.07960 8 + CD1 CW -0.18080 9 + HD1 H4 0.20430 10 + NE1 NA -0.33160 11 + HE1 H 0.34130 12 + CE2 CN 0.12220 13 + CZ2 CA -0.25940 14 + HZ2 HA 0.15670 15 + CH2 CA -0.10200 16 + HH2 HA 0.14010 17 + CZ3 CA -0.22870 18 + HZ3 HA 0.15070 19 + CE3 CA -0.18370 20 + HE3 HA 0.14910 21 + CD2 CB 0.10780 22 + C C 0.76580 23 + OC1 O2 -0.80110 24 + OC2 O2 -0.80110 25 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG CD1 + CG CD2 + CD1 HD1 + CD1 NE1 + NE1 HE1 + NE1 CE2 + CE2 CZ2 + CE2 CD2 + CZ2 HZ2 + CZ2 CH2 + CH2 HH2 + CH2 CZ3 + CZ3 HZ3 + CZ3 CE3 + CE3 HE3 + CE3 CD2 + C OC1 + C OC2 + -C N + [ impropers ] + -C CA N H + CA OC1 C OC2 + CD1 CE2 NE1 HE1 + CE2 CH2 CZ2 HZ2 + CZ2 CZ3 CH2 HH2 + CH2 CE3 CZ3 HZ3 + CZ3 CD2 CE3 HE3 + CG NE1 CD1 HD1 + CD1 CG CB CD2 + +[ CPHE ] + [ atoms ] + N N -0.38210 1 + H H 0.26810 2 + CA CX -0.18250 3 + HA H1 0.10980 4 + CB CT -0.09590 5 + HB1 HC 0.04430 6 + HB2 HC 0.04430 7 + CG CA 0.05520 8 + CD1 CA -0.13000 9 + HD1 HA 0.14080 10 + CE1 CA -0.18470 11 + HE1 HA 0.14610 12 + CZ CA -0.09440 13 + HZ HA 0.12800 14 + CE2 CA -0.18470 15 + HE2 HA 0.14610 16 + CD2 CA -0.13000 17 + HD2 HA 0.14080 18 + C C 0.76600 19 + OC1 O2 -0.80260 20 + OC2 O2 -0.80260 21 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG CD1 + CG CD2 + CD1 HD1 + CD1 CE1 + CE1 HE1 + CE1 CZ + CZ HZ + CZ CE2 + CE2 HE2 + CE2 CD2 + CD2 HD2 + C OC1 + C OC2 + -C N + [ impropers ] + -C CA N H + CA OC1 C OC2 + CG CE2 CD2 HD2 + CZ CD2 CE2 HE2 + CE1 CE2 CZ HZ + CD1 CZ CE1 HE1 + CG CE1 CD1 HD1 + CD1 CD2 CG CB + +[ CTYR ] + [ atoms ] + N N -0.38210 1 + H H 0.26810 2 + CA CX -0.20150 3 + HA H1 0.10920 4 + CB CT -0.07520 5 + HB1 HC 0.04900 6 + HB2 HC 0.04900 7 + CG CA 0.02430 8 + CD1 CA -0.19220 9 + HD1 HA 0.17800 10 + CE1 CA -0.24580 11 + HE1 HA 0.16730 12 + CZ C 0.33950 13 + OH OH -0.56430 14 + HH HO 0.40170 15 + CE2 CA -0.24580 16 + HE2 HA 0.16730 17 + CD2 CA -0.19220 18 + HD2 HA 0.17800 19 + C C 0.78170 20 + OC1 O2 -0.80700 21 + OC2 O2 -0.80700 22 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG CD1 + CG CD2 + CD1 HD1 + CD1 CE1 + CE1 HE1 + CE1 CZ + CZ OH + CZ CE2 + OH HH + CE2 HE2 + CE2 CD2 + CD2 HD2 + C OC1 + C OC2 + -C N + [ impropers ] + -C CA N H + CA OC1 C OC2 + CG CE2 CD2 HD2 + CZ CD2 CE2 HE2 + CD1 CZ CE1 HE1 + CG CE1 CD1 HD1 + CD1 CD2 CG CB + CE1 CE2 CZ OH + +[ CGLU ] + [ atoms ] + N N -0.51920 1 + H H 0.30550 2 + CA CX -0.20590 3 + HA H1 0.13990 4 + CB 2C 0.00710 5 + HB1 HC -0.00780 6 + HB2 HC -0.00780 7 + CG 2C 0.06750 8 + HG1 HC -0.05480 9 + HG2 HC -0.05480 10 + CD CO 0.81830 11 + OE1 O2 -0.82200 12 + OE2 O2 -0.82200 13 + C C 0.74200 14 + OC1 O2 -0.79300 15 + OC2 O2 -0.79300 16 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG HG1 + CG HG2 + CG CD + CD OE1 + CD OE2 + C OC1 + C OC2 + -C N + [ impropers ] + -C CA N H + CA OC1 C OC2 + CG OE1 CD OE2 + +[ CASP ] + [ atoms ] + N N -0.51920 1 + H H 0.30550 2 + CA CX -0.18170 3 + HA H1 0.10460 4 + CB 2C -0.06770 5 + HB1 HC -0.02120 6 + HB2 HC -0.02120 7 + CG CO 0.88510 8 + OD1 O2 -0.81620 9 + OD2 O2 -0.81620 10 + C C 0.72560 11 + OC1 O2 -0.78870 12 + OC2 O2 -0.78870 13 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG OD1 + CG OD2 + C OC1 + C OC2 + -C N + [ impropers ] + -C CA N H + CA OC1 C OC2 + CB OD1 CG OD2 + +[ CLYS ] + [ atoms ] + N N -0.34810 1 + H H 0.27640 2 + CA CX -0.29030 3 + HA H1 0.14380 4 + CB C8 -0.05380 5 + HB1 HC 0.04820 6 + HB2 HC 0.04820 7 + CG C8 0.02270 8 + HG1 HC 0.01340 9 + HG2 HC 0.01340 10 + CD C8 -0.03920 11 + HD1 HC 0.06110 12 + HD2 HC 0.06110 13 + CE C8 -0.01760 14 + HE1 HP 0.11210 15 + HE2 HP 0.11210 16 + NZ N3 -0.37410 17 + HZ1 H 0.33740 18 + HZ2 H 0.33740 19 + HZ3 H 0.33740 20 + C C 0.84880 21 + OC1 O2 -0.82520 22 + OC2 O2 -0.82520 23 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG HG1 + CG HG2 + CG CD + CD HD1 + CD HD2 + CD CE + CE HE1 + CE HE2 + CE NZ + NZ HZ1 + NZ HZ2 + NZ HZ3 + C OC1 + C OC2 + -C N + [ impropers ] + -C CA N H + CA OC1 C OC2 + +[ CPRO ] + [ atoms ] + N N -0.28020 1 + CD CT 0.04340 2 + HD1 H1 0.03310 3 + HD2 H1 0.03310 4 + CG CT 0.04660 5 + HG1 HC 0.01720 6 + HG2 HC 0.01720 7 + CB CT -0.05430 8 + HB1 HC 0.03810 9 + HB2 HC 0.03810 10 + CA CX -0.13360 11 + HA H1 0.07760 12 + C C 0.66310 13 + OC1 O2 -0.76970 14 + OC2 O2 -0.76970 15 + [ bonds ] + N CD + N CA + CD HD1 + CD HD2 + CD CG + CG HG1 + CG HG2 + CG CB + CB HB1 + CB HB2 + CB CA + CA HA + CA C + C OC1 + C OC2 + -C N + [ impropers ] + CA OC1 C OC2 + -C CD N CA + +[ CCYS ] + [ atoms ] + N N -0.38210 1 + H H 0.26810 2 + CA CX -0.16350 3 + HA H1 0.13960 4 + CB 2C -0.19960 5 + HB1 H1 0.14370 6 + HB2 H1 0.14370 7 + SG SH -0.31020 8 + HG HS 0.20680 9 + C C 0.74970 10 + OC1 O2 -0.79810 11 + OC2 O2 -0.79810 12 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB SG + SG HG + C OC1 + C OC2 + -C N + [ impropers ] + -C CA N H + CA OC1 C OC2 + +[ CCYX ] + [ atoms ] + N N -0.38210 1 + H H 0.26810 2 + CA CX -0.13180 3 + HA H1 0.09380 4 + CB 2C -0.19430 5 + HB1 H1 0.12280 6 + HB2 H1 0.12280 7 + SG S -0.05290 8 + C C 0.76180 9 + OC1 O2 -0.80410 10 + OC2 O2 -0.80410 11 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB SG + C OC1 + C OC2 + -C N + [ impropers ] + -C CA N H + CA OC1 C OC2 + +[ CMET ] + [ atoms ] + N N -0.38210 1 + H H 0.26810 2 + CA CX -0.25970 3 + HA H1 0.12770 4 + CB 2C -0.02360 5 + HB1 HC 0.04800 6 + HB2 HC 0.04800 7 + CG 2C 0.04920 8 + HG1 H1 0.03170 9 + HG2 H1 0.03170 10 + SD S -0.26920 11 + CE CT -0.03760 12 + HE1 H1 0.06250 13 + HE2 H1 0.06250 14 + HE3 H1 0.06250 15 + C C 0.80130 16 + OC1 O2 -0.81050 17 + OC2 O2 -0.81050 18 + [ bonds ] + N H + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG HG1 + CG HG2 + CG SD + SD CE + CE HE1 + CE HE2 + CE HE3 + C OC1 + C OC2 + -C N + [ impropers ] + -C CA N H + CA OC1 C OC2 + +; N-terminal AA's + +[ NALA ] + [ atoms ] + N N3 0.14140 1 + H1 H 0.19970 2 + H2 H 0.19970 3 + H3 H 0.19970 4 + CA CX 0.09620 5 + HA HP 0.08890 6 + CB CT -0.05970 7 + HB1 HC 0.03000 8 + HB2 HC 0.03000 9 + HB3 HC 0.03000 10 + C C 0.61630 11 + O O -0.57220 12 + [ bonds ] + N H1 + N H2 + N H3 + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB HB3 + C O + C +N + [ impropers ] + CA +N C O + +[ NGLY ] + [ atoms ] + N N3 0.29430 1 + H1 H 0.16420 2 + H2 H 0.16420 3 + H3 H 0.16420 4 + CA CX -0.01000 5 + HA1 HP 0.08950 6 + HA2 HP 0.08950 7 + C C 0.61630 8 + O O -0.57220 9 + [ bonds ] + N H1 + N H2 + N H3 + N CA + CA HA1 + CA HA2 + CA C + C O + C +N + [ impropers ] + CA +N C O + +[ NSER ] + [ atoms ] + N N3 0.18490 1 + H1 H 0.18980 2 + H2 H 0.18980 3 + H3 H 0.18980 4 + CA CX 0.05670 5 + HA HP 0.07820 6 + CB 2C 0.25960 7 + HB1 H1 0.02730 8 + HB2 H1 0.02730 9 + OG OH -0.67140 10 + HG HO 0.42390 11 + C C 0.61630 12 + O O -0.57220 13 + [ bonds ] + N H1 + N H2 + N H3 + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB OG + OG HG + C O + C +N + [ impropers ] + CA +N C O + +[ NTHR ] + [ atoms ] + N N3 0.18120 1 + H1 H 0.19340 2 + H2 H 0.19340 3 + H3 H 0.19340 4 + CA CX 0.00340 5 + HA HP 0.10870 6 + CB 3C 0.45140 7 + HB H1 -0.03230 8 + CG2 CT -0.25540 9 + HG21 HC 0.06270 10 + HG22 HC 0.06270 11 + HG23 HC 0.06270 12 + OG1 OH -0.67640 13 + HG1 HO 0.40700 14 + C C 0.61630 15 + O O -0.57220 16 + [ bonds ] + N H1 + N H2 + N H3 + N CA + CA HA + CA CB + CA C + CB HB + CB CG2 + CB OG1 + CG2 HG21 + CG2 HG22 + CG2 HG23 + OG1 HG1 + C O + C +N + [ impropers ] + CA +N C O + +[ NLEU ] + [ atoms ] + N N3 0.10100 1 + H1 H 0.21480 2 + H2 H 0.21480 3 + H3 H 0.21480 4 + CA CX 0.01040 5 + HA HP 0.10530 6 + CB 2C -0.02440 7 + HB1 HC 0.02560 8 + HB2 HC 0.02560 9 + CG 3C 0.34210 10 + HG HC -0.03800 11 + CD1 CT -0.41060 12 + HD11 HC 0.09800 13 + HD12 HC 0.09800 14 + HD13 HC 0.09800 15 + CD2 CT -0.41040 16 + HD21 HC 0.09800 17 + HD22 HC 0.09800 18 + HD23 HC 0.09800 19 + C C 0.61230 20 + O O -0.57130 21 + [ bonds ] + N H1 + N H2 + N H3 + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG HG + CG CD1 + CG CD2 + CD1 HD11 + CD1 HD12 + CD1 HD13 + CD2 HD21 + CD2 HD22 + CD2 HD23 + C O + C +N + [ impropers ] + CA +N C O + +[ NILE ] + [ atoms ] + N N3 0.03110 1 + H1 H 0.23290 2 + H2 H 0.23290 3 + H3 H 0.23290 4 + CA CX 0.02570 5 + HA HP 0.10310 6 + CB 3C 0.18850 7 + HB HC 0.02130 8 + CG2 CT -0.37200 9 + HG21 HC 0.09470 10 + HG22 HC 0.09470 11 + HG23 HC 0.09470 12 + CG1 2C -0.03870 13 + HG11 HC 0.02010 14 + HG12 HC 0.02010 15 + CD CT -0.09080 16 + HD1 HC 0.02260 17 + HD2 HC 0.02260 18 + HD3 HC 0.02260 19 + C C 0.61230 20 + O O -0.57130 21 + [ bonds ] + N H1 + N H2 + N H3 + N CA + CA HA + CA CB + CA C + CB HB + CB CG2 + CB CG1 + CG2 HG21 + CG2 HG22 + CG2 HG23 + CG1 HG11 + CG1 HG12 + CG1 CD + CD HD1 + CD HD2 + CD HD3 + C O + C +N + [ impropers ] + CA +N C O + +[ NVAL ] + [ atoms ] + N N3 0.05770 1 + H1 H 0.22720 2 + H2 H 0.22720 3 + H3 H 0.22720 4 + CA CX -0.00540 5 + HA HP 0.10930 6 + CB 3C 0.31960 7 + HB HC -0.02210 8 + CG1 CT -0.31290 9 + HG11 HC 0.07350 10 + HG12 HC 0.07350 11 + HG13 HC 0.07350 12 + CG2 CT -0.31290 13 + HG21 HC 0.07350 14 + HG22 HC 0.07350 15 + HG23 HC 0.07350 16 + C C 0.61630 17 + O O -0.57220 18 + [ bonds ] + N H1 + N H2 + N H3 + N CA + CA HA + CA CB + CA C + CB HB + CB CG1 + CB CG2 + CG1 HG11 + CG1 HG12 + CG1 HG13 + CG2 HG21 + CG2 HG22 + CG2 HG23 + C O + C +N + [ impropers ] + CA +N C O + +[ NASN ] + [ atoms ] + N N3 0.18010 1 + H1 H 0.19210 2 + H2 H 0.19210 3 + H3 H 0.19210 4 + CA CX 0.03680 5 + HA HP 0.12310 6 + CB 2C -0.02830 7 + HB1 HC 0.05150 8 + HB2 HC 0.05150 9 + CG C 0.58330 10 + OD1 O -0.57440 11 + ND2 N -0.86340 12 + HD21 H 0.40970 13 + HD22 H 0.40970 14 + C C 0.61630 15 + O O -0.57220 16 + [ bonds ] + N H1 + N H2 + N H3 + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG OD1 + CG ND2 + ND2 HD21 + ND2 HD22 + C O + C +N + [ impropers ] + CA +N C O + CB ND2 CG OD1 + CG HD21 ND2 HD22 + +[ NGLN ] + [ atoms ] + N N3 0.14930 1 + H1 H 0.19960 2 + H2 H 0.19960 3 + H3 H 0.19960 4 + CA CX 0.05360 5 + HA HP 0.10150 6 + CB 2C 0.06510 7 + HB1 HC 0.00500 8 + HB2 HC 0.00500 9 + CG 2C -0.09030 10 + HG1 HC 0.03310 11 + HG2 HC 0.03310 12 + CD C 0.73540 13 + OE1 O -0.61330 14 + NE2 N -1.00310 15 + HE21 H 0.44290 16 + HE22 H 0.44290 17 + C C 0.61230 18 + O O -0.57130 19 + [ bonds ] + N H1 + N H2 + N H3 + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG HG1 + CG HG2 + CG CD + CD OE1 + CD NE2 + NE2 HE21 + NE2 HE22 + C O + C +N + [ impropers ] + CA +N C O + CG NE2 CD OE1 + CD HE21 NE2 HE22 + +[ NARG ] + [ atoms ] + N N3 0.13050 1 + H1 H 0.20830 2 + H2 H 0.20830 3 + H3 H 0.20830 4 + CA CX -0.02230 5 + HA HP 0.12420 6 + CB C8 0.01180 7 + HB1 HC 0.02260 8 + HB2 HC 0.02260 9 + CG C8 0.02360 10 + HG1 HC 0.03090 11 + HG2 HC 0.03090 12 + CD C8 0.09350 13 + HD1 H1 0.05270 14 + HD2 H1 0.05270 15 + NE N2 -0.56500 16 + HE H 0.35920 17 + CZ CA 0.82810 18 + NH1 N2 -0.86930 19 + HH11 H 0.44940 20 + HH12 H 0.44940 21 + NH2 N2 -0.86930 22 + HH21 H 0.44940 23 + HH22 H 0.44940 24 + C C 0.72140 25 + O O -0.60130 26 + [ bonds ] + N H1 + N H2 + N H3 + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG HG1 + CG HG2 + CG CD + CD HD1 + CD HD2 + CD NE + NE HE + NE CZ + CZ NH1 + CZ NH2 + NH1 HH11 + NH1 HH12 + NH2 HH21 + NH2 HH22 + C O + C +N + [ impropers ] + CA +N C O + NE NH1 CZ NH2 + CD CZ NE HE + CZ HH11 NH1 HH12 + CZ HH21 NH2 HH22 + +[ NHID ] + [ atoms ] + N N3 0.15420 1 + H1 H 0.19630 2 + H2 H 0.19630 3 + H3 H 0.19630 4 + CA CX 0.09640 5 + HA HP 0.09580 6 + CB CT 0.02590 7 + HB1 HC 0.02090 8 + HB2 HC 0.02090 9 + CG CC -0.03990 10 + ND1 NA -0.38190 11 + HD1 H 0.36320 12 + CE1 CR 0.21270 13 + HE1 H5 0.13850 14 + NE2 NB -0.57110 15 + CD2 CV 0.10460 16 + HD2 H4 0.12990 17 + C C 0.61230 18 + O O -0.57130 19 + [ bonds ] + N H1 + N H2 + N H3 + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG ND1 + CG CD2 + ND1 HD1 + ND1 CE1 + CE1 HE1 + CE1 NE2 + NE2 CD2 + CD2 HD2 + C O + C +N + [ impropers ] + CA +N C O + CG CE1 ND1 HD1 + CG NE2 CD2 HD2 + ND1 NE2 CE1 HE1 + ND1 CD2 CG CB + +[ NHIE ] + [ atoms ] + N N3 0.14720 1 + H1 H 0.20160 2 + H2 H 0.20160 3 + H3 H 0.20160 4 + CA CX 0.02360 5 + HA HP 0.13800 6 + CB CT 0.04890 7 + HB1 HC 0.02230 8 + HB2 HC 0.02230 9 + CG CC 0.17400 10 + ND1 NB -0.55790 11 + CE1 CR 0.18040 12 + HE1 H5 0.13970 13 + NE2 NA -0.27810 14 + HE2 H 0.33240 15 + CD2 CW -0.23490 16 + HD2 H4 0.19630 17 + C C 0.61230 18 + O O -0.57130 19 + [ bonds ] + N H1 + N H2 + N H3 + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG ND1 + CG CD2 + ND1 CE1 + CE1 HE1 + CE1 NE2 + NE2 HE2 + NE2 CD2 + CD2 HD2 + C O + C +N + [ impropers ] + CA +N C O + CE1 CD2 NE2 HE2 + CG NE2 CD2 HD2 + ND1 NE2 CE1 HE1 + ND1 CD2 CG CB + +[ NHIP ] + [ atoms ] + N N3 0.25600 1 + H1 H 0.17040 2 + H2 H 0.17040 3 + H3 H 0.17040 4 + CA CX 0.05810 5 + HA HP 0.10470 6 + CB CT 0.04840 7 + HB1 HC 0.05310 8 + HB2 HC 0.05310 9 + CG CC -0.02360 10 + ND1 NA -0.15100 11 + HD1 H 0.38210 12 + CE1 CR -0.00110 13 + HE1 H5 0.26450 14 + NE2 NA -0.17390 15 + HE2 H 0.39210 16 + CD2 CW -0.14330 17 + HD2 H4 0.24950 18 + C C 0.72140 19 + O O -0.60130 20 + [ bonds ] + N H1 + N H2 + N H3 + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG ND1 + CG CD2 + ND1 HD1 + ND1 CE1 + CE1 HE1 + CE1 NE2 + NE2 HE2 + NE2 CD2 + CD2 HD2 + C O + C +N + [ impropers ] + CA +N C O + CG CE1 ND1 HD1 + CE1 CD2 NE2 HE2 + CG NE2 CD2 HD2 + ND1 NE2 CE1 HE1 + ND1 CD2 CG CB + +[ NTRP ] + [ atoms ] + N N3 0.19130 1 + H1 H 0.18880 2 + H2 H 0.18880 3 + H3 H 0.18880 4 + CA CX 0.04210 5 + HA HP 0.11620 6 + CB CT 0.05430 7 + HB1 HC 0.02220 8 + HB2 HC 0.02220 9 + CG C* -0.16540 10 + CD1 CW -0.17880 11 + HD1 H4 0.21950 12 + NE1 NA -0.34440 13 + HE1 H 0.34120 14 + CE2 CN 0.15750 15 + CZ2 CA -0.27100 16 + HZ2 HA 0.15890 17 + CH2 CA -0.10800 18 + HH2 HA 0.14110 19 + CZ3 CA -0.20340 20 + HZ3 HA 0.14580 21 + CE3 CA -0.22650 22 + HE3 HA 0.16460 23 + CD2 CB 0.11320 24 + C C 0.61230 25 + O O -0.57130 26 + [ bonds ] + N H1 + N H2 + N H3 + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG CD1 + CG CD2 + CD1 HD1 + CD1 NE1 + NE1 HE1 + NE1 CE2 + CE2 CZ2 + CE2 CD2 + CZ2 HZ2 + CZ2 CH2 + CH2 HH2 + CH2 CZ3 + CZ3 HZ3 + CZ3 CE3 + CE3 HE3 + CE3 CD2 + C O + C +N + [ impropers ] + CA +N C O + CD1 CE2 NE1 HE1 + CE2 CH2 CZ2 HZ2 + CZ2 CZ3 CH2 HH2 + CH2 CE3 CZ3 HZ3 + CZ3 CD2 CE3 HE3 + CG NE1 CD1 HD1 + CD1 CG CB CD2 + +[ NPHE ] + [ atoms ] + N N3 0.17370 1 + H1 H 0.19210 2 + H2 H 0.19210 3 + H3 H 0.19210 4 + CA CX 0.07330 5 + HA HP 0.10410 6 + CB CT 0.03300 7 + HB1 HC 0.01040 8 + HB2 HC 0.01040 9 + CG CA 0.00310 10 + CD1 CA -0.13920 11 + HD1 HA 0.13740 12 + CE1 CA -0.16020 13 + HE1 HA 0.14330 14 + CZ CA -0.12080 15 + HZ HA 0.13290 16 + CE2 CA -0.16030 17 + HE2 HA 0.14330 18 + CD2 CA -0.13910 19 + HD2 HA 0.13740 20 + C C 0.61230 21 + O O -0.57130 22 + [ bonds ] + N H1 + N H2 + N H3 + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG CD1 + CG CD2 + CD1 HD1 + CD1 CE1 + CE1 HE1 + CE1 CZ + CZ HZ + CZ CE2 + CE2 HE2 + CE2 CD2 + CD2 HD2 + C O + C +N + [ impropers ] + CA +N C O + CG CE2 CD2 HD2 + CZ CD2 CE2 HE2 + CE1 CE2 CZ HZ + CD1 CZ CE1 HE1 + CG CE1 CD1 HD1 + CD1 CD2 CG CB + +[ NTYR ] + [ atoms ] + N N3 0.19400 1 + H1 H 0.18730 2 + H2 H 0.18730 3 + H3 H 0.18730 4 + CA CX 0.05700 5 + HA HP 0.09830 6 + CB CT 0.06590 7 + HB1 HC 0.01020 8 + HB2 HC 0.01020 9 + CG CA -0.02050 10 + CD1 CA -0.20020 11 + HD1 HA 0.17200 12 + CE1 CA -0.22390 13 + HE1 HA 0.16500 14 + CZ C 0.31390 15 + OH OH -0.55780 16 + HH HO 0.40010 17 + CE2 CA -0.22390 18 + HE2 HA 0.16500 19 + CD2 CA -0.20020 20 + HD2 HA 0.17200 21 + C C 0.61230 22 + O O -0.57130 23 + [ bonds ] + N H1 + N H2 + N H3 + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG CD1 + CG CD2 + CD1 HD1 + CD1 CE1 + CE1 HE1 + CE1 CZ + CZ OH + CZ CE2 + OH HH + CE2 HE2 + CE2 CD2 + CD2 HD2 + C O + C +N + [ impropers ] + CA +N C O + CG CE2 CD2 HD2 + CZ CD2 CE2 HE2 + CD1 CZ CE1 HE1 + CG CE1 CD1 HD1 + CD1 CD2 CG CB + CE1 CE2 CZ OH + + +[ NGLU ] + [ atoms ] + N N3 0.00170 1 + H1 H 0.23910 2 + H2 H 0.23910 3 + H3 H 0.23910 4 + CA CX 0.05880 5 + HA HP 0.12020 6 + CB 2C 0.09090 7 + HB1 HC -0.02320 8 + HB2 HC -0.02320 9 + CG 2C -0.02360 10 + HG1 HC -0.03150 11 + HG2 HC -0.03150 12 + CD CO 0.80870 13 + OE1 O2 -0.81890 14 + OE2 O2 -0.81890 15 + C C 0.56210 16 + O O -0.58890 17 + [ bonds ] + N H1 + N H2 + N H3 + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG HG1 + CG HG2 + CG CD + CD OE1 + CD OE2 + C O + C +N + [ impropers ] + CA +N C O + CG OE1 CD OE2 + +[ NASP ] + [ atoms ] + N N3 0.07820 1 + H1 H 0.22000 2 + H2 H 0.22000 3 + H3 H 0.22000 4 + CA CX 0.02920 5 + HA HP 0.11410 6 + CB 2C -0.02350 7 + HB1 HC -0.01690 8 + HB2 HC -0.01690 9 + CG CO 0.81940 10 + OD1 O2 -0.80840 11 + OD2 O2 -0.80840 12 + C C 0.56210 13 + O O -0.58890 14 + [ bonds ] + N H1 + N H2 + N H3 + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG OD1 + CG OD2 + C O + C +N + [ impropers ] + CA +N C O + CB OD1 CG OD2 + +[ NLYS ] + [ atoms ] + N N3 0.09660 1 + H1 H 0.21650 2 + H2 H 0.21650 3 + H3 H 0.21650 4 + CA CX -0.00150 5 + HA HP 0.11800 6 + CB C8 0.02120 7 + HB1 HC 0.02830 8 + HB2 HC 0.02830 9 + CG C8 -0.00480 10 + HG1 HC 0.01210 11 + HG2 HC 0.01210 12 + CD C8 -0.06080 13 + HD1 HC 0.06330 14 + HD2 HC 0.06330 15 + CE C8 -0.01810 16 + HE1 HP 0.11710 17 + HE2 HP 0.11710 18 + NZ N3 -0.37640 19 + HZ1 H 0.33820 20 + HZ2 H 0.33820 21 + HZ3 H 0.33820 22 + C C 0.72140 23 + O O -0.60130 24 + [ bonds ] + N H1 + N H2 + N H3 + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG HG1 + CG HG2 + CG CD + CD HD1 + CD HD2 + CD CE + CE HE1 + CE HE2 + CE NZ + NZ HZ1 + NZ HZ2 + NZ HZ3 + C O + C +N + [ impropers ] + CA +N C O + +[ NPRO ] + [ atoms ] + N N3 -0.20200 1 + H1 H 0.31200 2 + H2 H 0.31200 3 + CD CT -0.01200 4 + HD1 HP 0.10000 5 + HD2 HP 0.10000 6 + CG CT -0.12100 7 + HG1 HC 0.10000 8 + HG2 HC 0.10000 9 + CB CT -0.11500 10 + HB1 HC 0.10000 11 + HB2 HC 0.10000 12 + CA CX 0.10000 13 + HA HP 0.10000 14 + C C 0.52600 15 + O O -0.50000 16 + [ bonds ] + N H1 + N H2 + N CD + N CA + CD HD1 + CD HD2 + CD CG + CG HG1 + CG HG2 + CG CB + CB HB1 + CB HB2 + CB CA + CA HA + CA C + C O + C +N + [ impropers ] + CA +N C O + +[ NCYS ] + [ atoms ] + N N3 0.13250 1 + H1 H 0.20230 2 + H2 H 0.20230 3 + H3 H 0.20230 4 + CA CX 0.09270 5 + HA HP 0.14110 6 + CB 2C -0.11950 7 + HB1 H1 0.11880 8 + HB2 H1 0.11880 9 + SG SH -0.32980 10 + HG HS 0.19750 11 + C C 0.61230 12 + O O -0.57130 13 + [ bonds ] + N H1 + N H2 + N H3 + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB SG + SG HG + C O + C +N + [ impropers ] + CA +N C O + +[ NCYX ] + [ atoms ] + N N3 0.20690 1 + H1 H 0.18150 2 + H2 H 0.18150 3 + H3 H 0.18150 4 + CA CX 0.10550 5 + HA HP 0.09220 6 + CB 2C -0.02770 7 + HB1 H1 0.06800 8 + HB2 H1 0.06800 9 + SG S -0.09840 10 + C C 0.61230 11 + O O -0.57130 12 + [ bonds ] + N H1 + N H2 + N H3 + N CA + CA HA + CA CB + CA C + CB HB2 + CB HB1 + CB SG + C O + C +N + [ impropers ] + CA +N C O + +[ NMET ] + [ atoms ] + N N3 0.15920 1 + H1 H 0.19840 2 + H2 H 0.19840 3 + H3 H 0.19840 4 + CA CX 0.02210 5 + HA HP 0.11160 6 + CB 2C 0.08650 7 + HB1 HC 0.01250 8 + HB2 HC 0.01250 9 + CG 2C 0.03340 10 + HG1 H1 0.02920 11 + HG2 H1 0.02920 12 + SD S -0.27740 13 + CE CT -0.03410 14 + HE1 H1 0.05970 15 + HE2 H1 0.05970 16 + HE3 H1 0.05970 17 + C C 0.61230 18 + O O -0.57130 19 + [ bonds ] + N H1 + N H2 + N H3 + N CA + CA HA + CA CB + CA C + CB HB1 + CB HB2 + CB CG + CG HG1 + CG HG2 + CG SD + SD CE + CE HE1 + CE HE2 + CE HE3 + C O + C +N + [ impropers ] + CA +N C O + diff --git a/pmx/data/mutff45/amber14sb_OL15.ff/aminoacids.vsd b/pmx/data/mutff45/amber14sb_OL15.ff/aminoacids.vsd new file mode 100644 index 00000000..fa52c674 --- /dev/null +++ b/pmx/data/mutff45/amber14sb_OL15.ff/aminoacids.vsd @@ -0,0 +1,225 @@ + +[ CH3 ] +; CT sp3 aliphatic C + ;bound to sp2/sp3 carbons + CT C MCH3 + CT CA MCH3 + CT CB MCH3 + CT CC MCH3 + CT CK MCH3 + CT CM MCH3 + CT CN MCH3 + CT CQ MCH3 + CT CR MCH3 + CT CT MCH3 + CT CV MCH3 + CT CW MCH3 + CT C* MCH3 + CT N MCH3 + CT S MCH3 + +[ NH3 ] +; N3 sp3 N for charged amino groups + N3 C MNH3 + N3 CA MNH3 + N3 CB MNH3 + N3 CC MNH3 + N3 CK MNH3 + N3 CM MNH3 + N3 CN MNH3 + N3 CQ MNH3 + N3 CR MNH3 + N3 CT MNH3 + N3 CV MNH3 + N3 CW MNH3 + N3 C* MNH3 + +[ NH2 ] +; N sp2 nitrogen in amide groups + N planar + N2 planar + + +; Data for generating dummy aromatic rings. +; Actually we dont need all these bonds and angles, +; but by specifying them here it is easier to improve +; the dummy generation code later. +[ PHE ] + CG CD1 0.140 + CG CD2 0.140 + CD1 CE1 0.140 + CD2 CE2 0.140 + CE1 CZ 0.140 + CE2 CZ 0.140 + CD1 HD1 0.108 + CD2 HD2 0.108 + CE1 HE1 0.108 + CE2 HE2 0.108 + CZ HZ 0.108 + CG CD1 CE1 120.0 + CD1 CE1 CZ 120.0 + CE1 CZ CE2 120.0 + CZ CE2 CD2 120.0 + CE2 CD2 CG 120.0 + CD2 CG CD1 120.0 + CG CD1 HD1 120.0 + CG CD2 HD2 120.0 + HD1 CD1 CE1 120.0 + CD1 CE1 HE1 120.0 + HE1 CE1 CZ 120.0 + CE1 CZ HZ 120.0 + HZ CZ CE2 120.0 + CZ CE2 HE2 120.0 + HE2 CE2 CD2 120.0 + HD2 CD2 CG 120.0 + + +[ TYR ] + CG CD1 0.140 + CG CD2 0.140 + CD1 CE1 0.140 + CD2 CE2 0.140 + CE1 CZ 0.140 + CE2 CZ 0.140 + CD1 HD1 0.108 + CD2 HD2 0.108 + CE1 HE1 0.108 + CE2 HE2 0.108 + CZ OH 0.1364 + OH HH 0.0945 + CG CD1 CE1 120.0 + CD1 CE1 CZ 120.0 + CE1 CZ CE2 120.0 + CZ CE2 CD2 120.0 + CE2 CD2 CG 120.0 + CD2 CG CD1 120.0 + CG CD1 HD1 120.0 + CG CD2 HD2 120.0 + HD1 CD1 CE1 120.0 + CD1 CE1 HE1 120.0 + HE1 CE1 CZ 120.0 + CE1 CZ OH 120.0 + CZ OH HH 113.0 + OH CZ CE2 120.0 + CZ CE2 HE2 120.0 + HE2 CE2 CD2 120.0 + HD2 CD2 CG 120.0 + + +[ TRP ] ; angles from MD sim, corrected to be perfectly planar + CB CG 0.1495 + CG CD1 0.1352 + CD1 NE1 0.1381 + NE1 CE2 0.138 + CE2 CD2 0.1419 + CD2 CG 0.1459 + CE2 CZ2 0.140 + CZ2 CH2 0.140 + CH2 CZ3 0.140 + CZ3 CE3 0.140 + CE3 CD2 0.1404 + CD1 HD1 0.108 + NE1 HE1 0.101 + CE3 HE3 0.108 + CZ2 HZ2 0.108 + CZ3 HZ3 0.108 + CH2 HH2 0.108 + CB CG CD1 125.6 + CB CG CD2 128.7 + CG CD1 NE1 109.7 + CD1 NE1 CE2 111.7 + NE1 CE2 CD2 104.3 + CE2 CD2 CG 108.5 + CD2 CG CD1 105.7 + NE1 CE2 CZ2 135.7 + CE2 CZ2 CH2 120.0 + CZ2 CH2 CZ3 120.0 + CH2 CZ3 CE3 120.0 + CZ3 CE3 CD2 120.0 + CE2 CD2 CE3 120.0 + CD2 CE2 CZ2 120.0 + CE3 CD2 CG 131.5 + CG CD1 HD1 124.6 + HD1 CD1 NE1 125.7 + CD1 NE1 HE1 122.7 + HE1 NE1 CE2 125.6 + CZ3 CE3 HE3 120.0 + HE3 CE3 CD2 120.0 + CE2 CZ2 HZ2 120.0 + HZ2 CZ2 CH2 120.0 + CH2 CZ3 HZ3 120.0 + HZ3 CZ3 CE3 120.0 + CZ2 CH2 HH2 120.0 + HH2 CH2 CZ3 120.0 + + +[ HID ] ; angles from MD sim, corrected to be perfectly planar + CG ND1 0.1381 + ND1 CE1 0.1343 + CE1 NE2 0.1335 + NE2 CD2 0.1394 + CD2 CG 0.137 + ND1 HD1 0.101 + CE1 HE1 0.108 + CD2 HD2 0.108 + CG ND1 CE1 107.4 + ND1 CE1 NE2 113.1 + CE1 NE2 CD2 104.5 + NE2 CD2 CG 109.3 + CD2 CG ND1 105.7 + CG ND1 HD1 127.7 + HD1 ND1 CE1 124.9 + ND1 CE1 HE1 122.9 + HE1 CE1 NE2 124.0 + CG CD2 HD2 129.0 + HD2 CD2 NE2 121.7 + + +[ HIE ] ; angles from MD sim, corrected to be perfectly planar + CG ND1 0.1394 + ND1 CE1 0.1335 + CE1 NE2 0.1343 + NE2 CD2 0.1381 + CD2 CG 0.137 + CD2 HD2 0.108 + CE1 HE1 0.108 + NE2 HE2 0.101 + CG ND1 CE1 104.9 + ND1 CE1 NE2 112.9 + CE1 NE2 CD2 107.3 + NE2 CD2 CG 106.4 + CD2 CG ND1 108.5 + NE2 CD2 HD2 122.4 + HD2 CD2 CG 131.2 + ND1 CE1 HE1 123.8 + HE1 CE1 NE2 123.3 + CE1 NE2 HE2 125.9 + HE2 NE2 CD2 126.8 + + +[ HIP ] ; angles from MD sim, corrected to be perfectly planar + CG ND1 0.1381 + ND1 CE1 0.1343 + CE1 NE2 0.1343 + NE2 CD2 0.1381 + CD2 CG 0.137 + ND1 HD1 0.101 + CE1 HE1 0.108 + CD2 HD2 0.108 + NE2 HE2 0.101 + CG ND1 CE1 107.4 + ND1 CE1 NE2 111.4 + CE1 NE2 CD2 107.2 + NE2 CD2 CG 107.4 + CD2 CG ND1 106.6 + CG ND1 HD1 127.4 + HD1 ND1 CE1 125.2 + ND1 CE1 HE1 124.3 + HE1 CE1 NE2 124.3 + CE1 NE2 HE2 126.4 + HE2 NE2 CD2 126.4 + NE2 CD2 HD2 120.8 + HD2 CD2 CG 131.8 + + + diff --git a/pmx/data/mutff45/amber14sb_OL15.ff/atomtypes.atp b/pmx/data/mutff45/amber14sb_OL15.ff/atomtypes.atp new file mode 100644 index 00000000..9b67750f --- /dev/null +++ b/pmx/data/mutff45/amber14sb_OL15.ff/atomtypes.atp @@ -0,0 +1,88 @@ +Br 79.90000 ; bromine +C 12.01000 ; sp2 C carbonyl group +CA 12.01000 ; sp2 C pure aromatic (benzene) +CB 12.01000 ; sp2 aromatic C, 5&6 membered ring junction +CC 12.01000 ; sp2 aromatic C, 5 memb. ring HIS +CD 12.01000 ; +CI 12.01000 ; sp3 Perez +CJ 12.01000 ; sp3 by Perez modified for Zgarbova and Jurecka +CK 12.01000 ; sp2 C 5 memb.ring in purines +CM 12.01000 ; sp2 C pyrimidines in pos. 5 & 6 +CN 12.01000 ; sp2 C aromatic 5&6 memb.ring junct.(TRP) +CO 12.01000 ; +CP 12.01000 ; +CQ 12.01000 ; sp2 C in 5 mem.ring of purines between 2 N +CR 12.01000 ; sp2 arom as CQ but in HIS +CS 12.01000 ; +CT 12.01000 ; sp3 aliphatic C +CV 12.01000 ; sp2 arom. 5 memb.ring w/1 N and 1 H (HIS) +CW 12.01000 ; sp2 arom. 5 memb.ring w/1 N-H and 1 H (HIS) +CX 12.01000 ; +CY 12.01000 ; +CZ 12.01000 ; +C* 12.01000 ; sp2 arom. 5 memb.ring w/1 subst. (TRP) +2C 12.01000 ; +3C 12.01000 ; +C1 12.01000 ; sp2 for chiOL4 +C2 12.01000 ; sp2 for chiOL4 +C4 12.01000 ; +C5 12.01000 ; +C7 12.01000 ; +C8 12.01000 ; +C0 40.08000 ; calcium +F 19.00000 ; fluorine +H 1.00800 ; H bonded to nitrogen atoms +HC 1.00800 ; H aliph. bond. to C without electrwd.group +H1 1.00800 ; H aliph. bond. to C with 1 electrwd. group +H2 1.00800 ; H aliph. bond. to C with 2 electrwd.groups +H3 1.00800 ; H aliph. bond. to C with 3 eletrwd.groups +HA 1.00800 ; H arom. bond. to C without elctrwd. groups +H4 1.00800 ; H arom. bond. to C with 1 electrwd. group +H5 1.00800 ; H arom. bond. to C with 2 electrwd. groups +HO 1.00800 ; hydroxyl group +HS 1.00800 ; hydrogen bonded to sulphur (pol?) +HW 1.00800 ; H in TIP3P water +HP 1.00800 ; H bonded to C next to positively charged gr +HZ 1.00800 ; +I 126.90000 ; iodine (Applequist) +Cl 35.45000 ; chlorine (Applequist) +Na 22.99000 ; Na+, ions pol:J.PhysC,11,1541,(1978) +IB 131.00000 ; 'big ion w/ waters' for vacuum (Na+, 6H2O) +MG 24.30500 ; magnesium +N 14.01000 ; sp2 nitrogen in amide groups +NA 14.01000 ; sp2 N in 5 memb.ring w/H atom (HIS) +NB 14.01000 ; sp2 N in 5 memb.ring w/LP (HIS,ADE,GUA) +NC 14.01000 ; sp2 N in 6 memb.ring w/LP (ADE,GUA) +N2 14.01000 ; sp2 N in amino groups +N3 14.01000 ; sp3 N for charged amino groups (Lys, etc) +N* 14.01000 ; sp2 N +NT 14.01000 ; +NY 14.01000 ; +O 16.00000 ; carbonyl group oxygen +OW 16.00000 ; oxygen in TIP3P water +OH 16.00000 ; oxygen in hydroxyl group +OS 16.00000 ; ether and ester oxygen +O2 16.00000 ; carboxyl and phosphate group oxygen +OP 16.00000 ; +P 30.97000 ; phosphate,pol:JACS,112,8543,90,K.J.Miller +S 32.06000 ; S in disulfide linkage,pol:JPC,102,2399,98 +SH 32.06000 ; S in cystine +CU 63.55000 ; copper +FE 55.00000 ; iron +K 39.10000 ; potassium +Rb 85.47000 ; rubidium +Cs 132.90000 ; cesium +OW_spc 15.99940 ; SPC Water OW +HW_spc 1.00800 ; SPC Water HW +Li 6.94000 ; lithium, ions pol:J.PhysC,11,1541,(1978) +Zn 65.40000 ; Zn2+ +HW_tip4pew 1.00800 ; tip4pEW HW +OW_tip4pew 16.00000 ; tip4pEW OW +HW_tip4p 1.00800 ; tip4p HW +OW_tip4p 16.00000 ; tip4p OW +HW_tip5p 1.00800 ; tip5p HW +OW_tip5p 16.00000 ; tip5p OW +MW 0.00000 ; Virtual site for tip4p/5p water point charge +MCH3 0.00000 ; Dummy mass in rigid tetrahedral CH3 group +MNH3 0.00000 ; Dummy mass in rigid tetrahedral NH3 group +EP 0.00000 ; diff --git a/pmx/data/mutff45/amber14sb_OL15.ff/dna.arn b/pmx/data/mutff45/amber14sb_OL15.ff/dna.arn new file mode 100644 index 00000000..ef4db904 --- /dev/null +++ b/pmx/data/mutff45/amber14sb_OL15.ff/dna.arn @@ -0,0 +1,8 @@ +DNA OP1 O1P +DNA OP2 O2P +DNA H2' H2'1 +DNA H2'' H2'2 +DNA H5' H5'1 +DNA H5'' H5'2 +DNA HO5' H5T +DNA HO3' H3T diff --git a/pmx/data/mutff45/amber14sb_OL15.ff/dna.hdb b/pmx/data/mutff45/amber14sb_OL15.ff/dna.hdb new file mode 100644 index 00000000..09f83f7c --- /dev/null +++ b/pmx/data/mutff45/amber14sb_OL15.ff/dna.hdb @@ -0,0 +1,160 @@ +DA5 9 +1 2 H5T O5' C5' C4' +2 6 H5' C5' O5' C4' +1 5 H4' C4' C5' O4' C3' +1 5 H1' C1' O4' N9 C2' +1 1 H8 C8 N9 N7 +2 3 H6 N6 C6 C5 +1 1 H2 C2 N1 N3 +1 5 H3' C3' C4' C2' O3' +2 6 H2' C2' C1' C3' +DA 8 +2 6 H5' C5' O5' C4' +1 5 H4' C4' C5' O4' C3' +1 5 H1' C1' O4' N9 C2' +1 1 H8 C8 N9 N7 +2 3 H6 N6 C6 C5 +1 1 H2 C2 N1 N3 +1 5 H3' C3' C4' C2' O3' +2 6 H2' C2' C1' C3' +DA3 9 +2 6 H5' C5' O5' C4' +1 5 H4' C4' C5' O4' C3' +1 5 H1' C1' O4' N9 C2' +1 1 H8 C8 N9 N7 +2 3 H6 N6 C6 C5 +1 1 H2 C2 N1 N3 +1 5 H3' C3' C4' C2' O3' +2 6 H2' C2' C1' C3' +1 2 H3T O3' C3' C4' +DAN 10 +1 2 H5T O5' C5' C4' +2 6 H5' C5' O5' C4' +1 5 H4' C4' C5' O4' C3' +1 5 H1' C1' O4' N9 C2' +1 1 H8 C8 N9 N7 +2 3 H6 N6 C6 C5 +1 1 H2 C2 N1 N3 +1 5 H3' C3' C4' C2' O3' +2 6 H2' C2' C1' C3' +1 2 H3T O3' C3' C4' +DT5 9 +1 2 H5T O5' C5' C4' +2 6 H5' C5' O5' C4' +1 5 H4' C4' C5' O4' C3' +1 5 H1' C1' O4' N1 C2' +1 1 H6 C6 N1 C5 +3 4 H7 C7 C5 C6 +1 1 H3 N3 C4 C2 +1 5 H3' C3' C4' C2' O3' +2 6 H2' C2' C1' C3' +DT 8 +2 6 H5' C5' O5' C4' +1 5 H4' C4' C5' O4' C3' +1 5 H1' C1' O4' N1 C2' +1 1 H6 C6 N1 C5 +3 4 H7 C7 C5 C6 +1 1 H3 N3 C4 C2 +1 5 H3' C3' C4' C2' O3' +2 6 H2' C2' C1' C3' +DT3 9 +2 6 H5' C5' O5' C4' +1 5 H4' C4' C5' O4' C3' +1 5 H1' C1' O4' N1 C2' +1 1 H6 C6 N1 C5 +3 4 H7 C7 C5 C6 +1 1 H3 N3 C4 C2 +1 5 H3' C3' C4' C2' O3' +2 6 H2' C2' C1' C3' +1 2 H3T O3' C3' C4' +DTN 10 +1 2 H5T O5' C5' C4' +2 6 H5' C5' O5' C4' +1 5 H4' C4' C5' O4' C3' +1 5 H1' C1' O4' N1 C2' +1 1 H6 C6 N1 C5 +3 4 H7 C7 C5 C6 +1 1 H3 N3 C4 C2 +1 5 H3' C3' C4' C2' O3' +2 6 H2' C2' C1' C3' +1 2 H3T O3' C3' C4' +DG5 9 +1 2 H5T O5' C5' C4' +2 6 H5' C5' O5' C4' +1 5 H4' C4' C5' O4' C3' +1 5 H1' C1' O4' N9 C2' +1 1 H8 C8 N9 N7 +1 1 H1 N1 C6 C2 +2 3 H2 N2 C2 N1 +1 5 H3' C3' C4' C2' O3' +2 6 H2' C2' C1' C3' +DG 8 +2 6 H5' C5' O5' C4' +1 5 H4' C4' C5' O4' C3' +1 5 H1' C1' O4' N9 C2' +1 1 H8 C8 N9 N7 +1 1 H1 N1 C6 C2 +2 3 H2 N2 C2 N1 +1 5 H3' C3' C4' C2' O3' +2 6 H2' C2' C1' C3' +DG3 9 +2 6 H5' C5' O5' C4' +1 5 H4' C4' C5' O4' C3' +1 5 H1' C1' O4' N9 C2' +1 1 H8 C8 N9 N7 +1 1 H1 N1 C6 C2 +2 3 H2 N2 C2 N1 +1 5 H3' C3' C4' C2' O3' +2 6 H2' C2' C1' C3' +1 2 H3T O3' C3' C4' +DGN 10 +1 2 H5T O5' C5' C4' +2 6 H5' C5' O5' C4' +1 5 H4' C4' C5' O4' C3' +1 5 H1' C1' O4' N9 C2' +1 1 H8 C8 N9 N7 +1 1 H1 N1 C6 C2 +2 3 H2 N2 C2 N1 +1 5 H3' C3' C4' C2' O3' +2 6 H2' C2' C1' C3' +1 2 H3T O3' C3' C4' +DC5 9 +1 2 H5T O5' C5' C4' +2 6 H5' C5' O5' C4' +1 5 H4' C4' C5' O4' C3' +1 5 H1' C1' O4' N1 C2' +1 1 H6 C6 N1 C5 +1 1 H5 C5 C6 C4 +2 3 H4 N4 C4 C5 +1 5 H3' C3' C4' C2' O3' +2 6 H2' C2' C1' C3' +DC 8 +2 6 H5' C5' O5' C4' +1 5 H4' C4' C5' O4' C3' +1 5 H1' C1' O4' N1 C2' +1 1 H6 C6 N1 C5 +1 1 H5 C5 C6 C4 +2 3 H4 N4 C4 C5 +1 5 H3' C3' C4' C2' O3' +2 6 H2' C2' C1' C3' +DC3 9 +2 6 H5' C5' O5' C4' +1 5 H4' C4' C5' O4' C3' +1 5 H1' C1' O4' N1 C2' +1 1 H6 C6 N1 C5 +1 1 H5 C5 C6 C4 +2 3 H4 N4 C4 C5 +1 5 H3' C3' C4' C2' O3' +2 6 H2' C2' C1' C3' +1 2 H3T O3' C3' C4' +DCN 10 +1 2 H5T O5' C5' C4' +2 6 H5' C5' O5' C4' +1 5 H4' C4' C5' O4' C3' +1 5 H1' C1' O4' N1 C2' +1 1 H6 C6 N1 C5 +1 1 H5 C5 C6 C4 +2 3 H4 N4 C4 C5 +1 5 H3' C3' C4' C2' O3' +2 6 H2' C2' C1' C3' +1 2 H3T O3' C3' C4' diff --git a/pmx/data/mutff45/amber14sb_OL15.ff/dna.r2b b/pmx/data/mutff45/amber14sb_OL15.ff/dna.r2b new file mode 100644 index 00000000..285ec3b2 --- /dev/null +++ b/pmx/data/mutff45/amber14sb_OL15.ff/dna.r2b @@ -0,0 +1,6 @@ +; rtp residue to rtp building block table +;GMX Force-field +DA DA DA5 DA3 DAN +DG DG DG5 DG3 DGN +DC DC DC5 DC3 DCN +DT DT DT5 DT3 DTN diff --git a/pmx/data/mutff45/amber14sb_OL15.ff/dna.rtp b/pmx/data/mutff45/amber14sb_OL15.ff/dna.rtp new file mode 100644 index 00000000..0585c563 --- /dev/null +++ b/pmx/data/mutff45/amber14sb_OL15.ff/dna.rtp @@ -0,0 +1,1223 @@ +[ bondedtypes ] +; Col 1: Type of bond +; Col 2: Type of angles +; Col 3: Type of proper dihedrals +; Col 4: Type of improper dihedrals +; Col 5: Generate all dihedrals if 1, only heavy atoms of 0. +; Col 6: Number of excluded neighbors for nonbonded interactions +; Col 7: Generate 1,4 interactions between pairs of hydrogens if 1 +; Col 8: Remove impropers over the same bond as a proper if it is 1 +; bonds angles dihedrals impropers all_dihedrals nrexcl HH14 RemoveDih + 1 1 9 4 1 3 1 0 + + +; 5' (XXF), 3' (XXT), non-terminal (XX), and monomer (XXN) nuc's + +[ DA5 ] + [ atoms ] + H5T HO 0.44220 1 + O5' OH -0.63180 2 + C5' CJ -0.00690 3 + H5'1 H1 0.07540 4 + H5'2 H1 0.07540 5 + C4' CT 0.16290 6 + H4' H1 0.11760 7 + O4' OS -0.36910 8 + C1' CT 0.04310 9 + H1' H2 0.18380 10 + N9 N* -0.02680 11 + C8 C2 0.16070 12 + H8 H5 0.18770 13 + N7 NB -0.61750 14 + C5 CB 0.07250 15 + C6 CA 0.68970 16 + N6 N2 -0.91230 17 + H61 H 0.41670 18 + H62 H 0.41670 19 + N1 NC -0.76240 20 + C2 CQ 0.57160 21 + H2 H5 0.05980 22 + N3 NC -0.74170 23 + C4 CB 0.38000 24 + C3' C7 0.07130 25 + H3' H1 0.09850 26 + C2' CT -0.08540 27 + H2'1 HC 0.07180 28 + H2'2 HC 0.07180 29 + O3' OS -0.52320 30 + [ bonds ] + H5T O5' + O5' C5' + C5' H5'1 + C5' H5'2 + C5' C4' + C4' H4' + C4' O4' + C4' C3' + O4' C1' + C1' H1' + C1' N9 + C1' C2' + N9 C8 + N9 C4 + C8 H8 + C8 N7 + N7 C5 + C5 C6 + C5 C4 + C6 N6 + C6 N1 + N6 H61 + N6 H62 + N1 C2 + C2 H2 + C2 N3 + N3 C4 + C3' H3' + C3' C2' + C3' O3' + C2' H2'1 + C2' H2'2 + [ impropers ] + C4 C8 N9 C1' + C6 H61 N6 H62 + N9 N7 C8 H8 + N1 N3 C2 H2 + C5 N6 C6 N1 + +[ DA ] + [ atoms ] + P P 1.16590 1 + O1P O2 -0.77610 2 + O2P O2 -0.77610 3 + O5' OS -0.49540 4 + C5' CJ -0.00690 5 + H5'1 H1 0.07540 6 + H5'2 H1 0.07540 7 + C4' CT 0.16290 8 + H4' H1 0.11760 9 + O4' OS -0.36910 10 + C1' CT 0.04310 11 + H1' H2 0.18380 12 + N9 N* -0.02680 13 + C8 C2 0.16070 14 + H8 H5 0.18770 15 + N7 NB -0.61750 16 + C5 CB 0.07250 17 + C6 CA 0.68970 18 + N6 N2 -0.91230 19 + H61 H 0.41670 20 + H62 H 0.41670 21 + N1 NC -0.76240 22 + C2 CQ 0.57160 23 + H2 H5 0.05980 24 + N3 NC -0.74170 25 + C4 CB 0.38000 26 + C3' C7 0.07130 27 + H3' H1 0.09850 28 + C2' CT -0.08540 29 + H2'1 HC 0.07180 30 + H2'2 HC 0.07180 31 + O3' OS -0.52320 32 + [ bonds ] + P O1P + P O2P + P O5' + O5' C5' + C5' H5'1 + C5' H5'2 + C5' C4' + C4' H4' + C4' O4' + C4' C3' + O4' C1' + C1' H1' + C1' N9 + C1' C2' + N9 C8 + N9 C4 + C8 H8 + C8 N7 + N7 C5 + C5 C6 + C5 C4 + C6 N6 + C6 N1 + N6 H61 + N6 H62 + N1 C2 + C2 H2 + C2 N3 + N3 C4 + C3' H3' + C3' C2' + C3' O3' + C2' H2'1 + C2' H2'2 + -O3' P + [ impropers ] + C4 C8 N9 C1' + C6 H61 N6 H62 + N9 N7 C8 H8 + N1 N3 C2 H2 + C5 N6 C6 N1 + +[ DA3 ] + [ atoms ] + P P 1.16590 1 + O1P O2 -0.77610 2 + O2P O2 -0.77610 3 + O5' OS -0.49540 4 + C5' CJ -0.00690 5 + H5'1 H1 0.07540 6 + H5'2 H1 0.07540 7 + C4' CT 0.16290 8 + H4' H1 0.11760 9 + O4' OS -0.36910 10 + C1' CT 0.04310 11 + H1' H2 0.18380 12 + N9 N* -0.02680 13 + C8 C2 0.16070 14 + H8 H5 0.18770 15 + N7 NB -0.61750 16 + C5 CB 0.07250 17 + C6 CA 0.68970 18 + N6 N2 -0.91230 19 + H61 H 0.41670 20 + H62 H 0.41670 21 + N1 NC -0.76240 22 + C2 CQ 0.57160 23 + H2 H5 0.05980 24 + N3 NC -0.74170 25 + C4 CB 0.38000 26 + C3' C7 0.07130 27 + H3' H1 0.09850 28 + C2' CT -0.08540 29 + H2'1 HC 0.07180 30 + H2'2 HC 0.07180 31 + O3' OH -0.65490 32 + H3T HO 0.43960 33 + [ bonds ] + P O1P + P O2P + P O5' + O5' C5' + C5' H5'1 + C5' H5'2 + C5' C4' + C4' H4' + C4' O4' + C4' C3' + O4' C1' + C1' H1' + C1' N9 + C1' C2' + N9 C8 + N9 C4 + C8 H8 + C8 N7 + N7 C5 + C5 C6 + C5 C4 + C6 N6 + C6 N1 + N6 H61 + N6 H62 + N1 C2 + C2 H2 + C2 N3 + N3 C4 + C3' H3' + C3' C2' + C3' O3' + C2' H2'1 + C2' H2'2 + O3' H3T + -O3' P + [ impropers ] + C4 C8 N9 C1' + C6 H61 N6 H62 + N9 N7 C8 H8 + N1 N3 C2 H2 + C5 N6 C6 N1 + +[ DAN ] + [ atoms ] + H5T HO 0.44220 1 + O5' OH -0.63180 2 + C5' CJ -0.00690 3 + H5'1 H1 0.07540 4 + H5'2 H1 0.07540 5 + C4' CT 0.16290 6 + H4' H1 0.11760 7 + O4' OS -0.36910 8 + C1' CT 0.04310 9 + H1' H2 0.18380 10 + N9 N* -0.02680 11 + C8 C2 0.16070 12 + H8 H5 0.18770 13 + N7 NB -0.61750 14 + C5 CB 0.07250 15 + C6 CA 0.68970 16 + N6 N2 -0.91230 17 + H61 H 0.41670 18 + H62 H 0.41670 19 + N1 NC -0.76240 20 + C2 CQ 0.57160 21 + H2 H5 0.05980 22 + N3 NC -0.74170 23 + C4 CB 0.38000 24 + C3' C7 0.07130 25 + H3' H1 0.09850 26 + C2' CT -0.08540 27 + H2'1 HC 0.07180 28 + H2'2 HC 0.07180 29 + O3' OH -0.65490 30 + H3T HO 0.43960 31 + [ bonds ] + H5T O5' + O5' C5' + C5' H5'1 + C5' H5'2 + C5' C4' + C4' H4' + C4' O4' + C4' C3' + O4' C1' + C1' H1' + C1' N9 + C1' C2' + N9 C8 + N9 C4 + C8 H8 + C8 N7 + N7 C5 + C5 C6 + C5 C4 + C6 N6 + C6 N1 + N6 H61 + N6 H62 + N1 C2 + C2 H2 + C2 N3 + N3 C4 + C3' H3' + C3' C2' + C3' O3' + C2' H2'1 + C2' H2'2 + O3' H3T + [ impropers ] + C4 C8 N9 C1' + C6 H61 N6 H62 + N9 N7 C8 H8 + N1 N3 C2 H2 + C5 N6 C6 N1 + + + +[ DT5 ] + [ atoms ] + H5T HO 0.44220 1 + O5' OH -0.63180 2 + C5' CJ -0.00690 3 + H5'1 H1 0.07540 4 + H5'2 H1 0.07540 5 + C4' CT 0.16290 6 + H4' H1 0.11760 7 + O4' OS -0.36910 8 + C1' CT 0.06800 9 + H1' H2 0.18040 10 + N1 N* -0.02390 11 + C6 CM -0.22090 12 + H6 H4 0.26070 13 + C5 CM 0.00250 14 + C7 CT -0.22690 15 + H71 HC 0.07700 16 + H72 HC 0.07700 17 + H73 HC 0.07700 18 + C4 C 0.51940 19 + O4 O -0.55630 20 + N3 NA -0.43400 21 + H3 H 0.34200 22 + C2 C 0.56770 23 + O2 O -0.58810 24 + C3' C7 0.07130 25 + H3' H1 0.09850 26 + C2' CT -0.08540 27 + H2'1 HC 0.07180 28 + H2'2 HC 0.07180 29 + O3' OS -0.52320 30 + [ bonds ] + H5T O5' + O5' C5' + C5' H5'1 + C5' H5'2 + C5' C4' + C4' H4' + C4' O4' + C4' C3' + O4' C1' + C1' H1' + C1' N1 + C1' C2' + N1 C6 + N1 C2 + C6 H6 + C6 C5 + C5 C7 + C5 C4 + C7 H71 + C7 H72 + C7 H73 + C4 O4 + C4 N3 + N3 H3 + N3 C2 + C2 O2 + C3' H3' + C3' C2' + C3' O3' + C2' H2'1 + C2' H2'2 + [ impropers ] + C2 C6 N1 C1' + C4 C6 C5 C7 + N1 N3 C2 O2 + C5 N3 C4 O4 + C4 C2 N3 H3 + N1 C5 C6 H6 + +[ DT ] + [ atoms ] + P P 1.16590 1 + O1P O2 -0.77610 2 + O2P O2 -0.77610 3 + O5' OS -0.49540 4 + C5' CJ -0.00690 5 + H5'1 H1 0.07540 6 + H5'2 H1 0.07540 7 + C4' CT 0.16290 8 + H4' H1 0.11760 9 + O4' OS -0.36910 10 + C1' CT 0.06800 11 + H1' H2 0.18040 12 + N1 N* -0.02390 13 + C6 CM -0.22090 14 + H6 H4 0.26070 15 + C5 CM 0.00250 16 + C7 CT -0.22690 17 + H71 HC 0.07700 18 + H72 HC 0.07700 19 + H73 HC 0.07700 20 + C4 C 0.51940 21 + O4 O -0.55630 22 + N3 NA -0.43400 23 + H3 H 0.34200 24 + C2 C 0.56770 25 + O2 O -0.58810 26 + C3' C7 0.07130 27 + H3' H1 0.09850 28 + C2' CT -0.08540 29 + H2'1 HC 0.07180 30 + H2'2 HC 0.07180 31 + O3' OS -0.52320 32 + [ bonds ] + P O1P + P O2P + P O5' + O5' C5' + C5' H5'1 + C5' H5'2 + C5' C4' + C4' H4' + C4' O4' + C4' C3' + O4' C1' + C1' H1' + C1' N1 + C1' C2' + N1 C6 + N1 C2 + C6 H6 + C6 C5 + C5 C7 + C5 C4 + C7 H71 + C7 H72 + C7 H73 + C4 O4 + C4 N3 + N3 H3 + N3 C2 + C2 O2 + C3' H3' + C3' C2' + C3' O3' + C2' H2'1 + C2' H2'2 + -O3' P + [ impropers ] + C2 C6 N1 C1' + C4 C6 C5 C7 + N1 N3 C2 O2 + C5 N3 C4 O4 + C4 C2 N3 H3 + N1 C5 C6 H6 + +[ DT3 ] + [ atoms ] + P P 1.16590 1 + O1P O2 -0.77610 2 + O2P O2 -0.77610 3 + O5' OS -0.49540 4 + C5' CJ -0.00690 5 + H5'1 H1 0.07540 6 + H5'2 H1 0.07540 7 + C4' CT 0.16290 8 + H4' H1 0.11760 9 + O4' OS -0.36910 10 + C1' CT 0.06800 11 + H1' H2 0.18040 12 + N1 N* -0.02390 13 + C6 CM -0.22090 14 + H6 H4 0.26070 15 + C5 CM 0.00250 16 + C7 CT -0.22690 17 + H71 HC 0.07700 18 + H72 HC 0.07700 19 + H73 HC 0.07700 20 + C4 C 0.51940 21 + O4 O -0.55630 22 + N3 NA -0.43400 23 + H3 H 0.34200 24 + C2 C 0.56770 25 + O2 O -0.58810 26 + C3' C7 0.07130 27 + H3' H1 0.09850 28 + C2' CT -0.08540 29 + H2'1 HC 0.07180 30 + H2'2 HC 0.07180 31 + O3' OH -0.65490 32 + H3T HO 0.43960 33 + [ bonds ] + P O1P + P O2P + P O5' + O5' C5' + C5' H5'1 + C5' H5'2 + C5' C4' + C4' H4' + C4' O4' + C4' C3' + O4' C1' + C1' H1' + C1' N1 + C1' C2' + N1 C6 + N1 C2 + C6 H6 + C6 C5 + C5 C7 + C5 C4 + C7 H71 + C7 H72 + C7 H73 + C4 O4 + C4 N3 + N3 H3 + N3 C2 + C2 O2 + C3' H3' + C3' C2' + C3' O3' + C2' H2'1 + C2' H2'2 + O3' H3T + -O3' P + [ impropers ] + C2 C6 N1 C1' + C4 C6 C5 C7 + N1 N3 C2 O2 + C5 N3 C4 O4 + C4 C2 N3 H3 + N1 C5 C6 H6 + +[ DTN ] + [ atoms ] + H5T HO 0.44220 1 + O5' OH -0.63180 2 + C5' CJ -0.00690 3 + H5'1 H1 0.07540 4 + H5'2 H1 0.07540 5 + C4' CT 0.16290 6 + H4' H1 0.11760 7 + O4' OS -0.36910 8 + C1' CT 0.06800 9 + H1' H2 0.18040 10 + N1 N* -0.02390 11 + C6 CM -0.22090 12 + H6 H4 0.26070 13 + C5 CM 0.00250 14 + C7 CT -0.22690 15 + H71 HC 0.07700 16 + H72 HC 0.07700 17 + H73 HC 0.07700 18 + C4 C 0.51940 19 + O4 O -0.55630 20 + N3 NA -0.43400 21 + H3 H 0.34200 22 + C2 C 0.56770 23 + O2 O -0.58810 24 + C3' C7 0.07130 25 + H3' H1 0.09850 26 + C2' CT -0.08540 27 + H2'1 HC 0.07180 28 + H2'2 HC 0.07180 29 + O3' OH -0.65490 30 + H3T HO 0.43960 31 + [ bonds ] + H5T O5' + O5' C5' + C5' H5'1 + C5' H5'2 + C5' C4' + C4' H4' + C4' O4' + C4' C3' + O4' C1' + C1' H1' + C1' N1 + C1' C2' + N1 C6 + N1 C2 + C6 H6 + C6 C5 + C5 C7 + C5 C4 + C7 H71 + C7 H72 + C7 H73 + C4 O4 + C4 N3 + N3 H3 + N3 C2 + C2 O2 + C3' H3' + C3' C2' + C3' O3' + C2' H2'1 + C2' H2'2 + O3' H3T + [ impropers ] + C2 C6 N1 C1' + C4 C6 C5 C7 + N1 N3 C2 O2 + C5 N3 C4 O4 + C4 C2 N3 H3 + N1 C5 C6 H6 + + + +[ DG5 ] + [ atoms ] + H5T HO 0.44220 1 + O5' OH -0.63180 2 + C5' CJ -0.00690 3 + H5'1 H1 0.07540 4 + H5'2 H1 0.07540 5 + C4' CT 0.16290 6 + H4' H1 0.11760 7 + O4' OS -0.36910 8 + C1' CT 0.03580 9 + H1' H2 0.17460 10 + N9 N* 0.05770 11 + C8 CK 0.07360 12 + H8 H5 0.19970 13 + N7 NB -0.57250 14 + C5 CB 0.19910 15 + C6 C 0.49180 16 + O6 O -0.56990 17 + N1 NA -0.50530 18 + H1 H 0.35200 19 + C2 CA 0.74320 20 + N2 N2 -0.92300 21 + H21 H 0.42350 22 + H22 H 0.42350 23 + N3 NC -0.66360 24 + C4 CB 0.18140 25 + C3' C7 0.07130 26 + H3' H1 0.09850 27 + C2' CT -0.08540 28 + H2'1 HC 0.07180 29 + H2'2 HC 0.07180 30 + O3' OS -0.52320 31 + [ bonds ] + H5T O5' + O5' C5' + C5' H5'1 + C5' H5'2 + C5' C4' + C4' H4' + C4' O4' + C4' C3' + O4' C1' + C1' H1' + C1' N9 + C1' C2' + N9 C8 + N9 C4 + C8 H8 + C8 N7 + N7 C5 + C5 C6 + C5 C4 + C6 O6 + C6 N1 + N1 H1 + N1 C2 + C2 N2 + C2 N3 + N2 H21 + N2 H22 + N3 C4 + C3' H3' + C3' C2' + C3' O3' + C2' H2'1 + C2' H2'2 + [ impropers ] + C4 C8 N9 C1' + C5 N1 C6 O6 + C6 C2 N1 H1 + C2 H21 N2 H22 + N9 N7 C8 H8 + N2 N1 C2 N3 + +[ DG ] + [ atoms ] + P P 1.16590 1 + O1P O2 -0.77610 2 + O2P O2 -0.77610 3 + O5' OS -0.49540 4 + C5' CJ -0.00690 5 + H5'1 H1 0.07540 6 + H5'2 H1 0.07540 7 + C4' CT 0.16290 8 + H4' H1 0.11760 9 + O4' OS -0.36910 10 + C1' CT 0.03580 11 + H1' H2 0.17460 12 + N9 N* 0.05770 13 + C8 CK 0.07360 14 + H8 H5 0.19970 15 + N7 NB -0.57250 16 + C5 CB 0.19910 17 + C6 C 0.49180 18 + O6 O -0.56990 19 + N1 NA -0.50530 20 + H1 H 0.35200 21 + C2 CA 0.74320 22 + N2 N2 -0.92300 23 + H21 H 0.42350 24 + H22 H 0.42350 25 + N3 NC -0.66360 26 + C4 CB 0.18140 27 + C3' C7 0.07130 28 + H3' H1 0.09850 29 + C2' CT -0.08540 30 + H2'1 HC 0.07180 31 + H2'2 HC 0.07180 32 + O3' OS -0.52320 33 + [ bonds ] + P O1P + P O2P + P O5' + O5' C5' + C5' H5'1 + C5' H5'2 + C5' C4' + C4' H4' + C4' O4' + C4' C3' + O4' C1' + C1' H1' + C1' N9 + C1' C2' + N9 C8 + N9 C4 + C8 H8 + C8 N7 + N7 C5 + C5 C6 + C5 C4 + C6 O6 + C6 N1 + N1 H1 + N1 C2 + C2 N2 + C2 N3 + N2 H21 + N2 H22 + N3 C4 + C3' H3' + C3' C2' + C3' O3' + C2' H2'1 + C2' H2'2 + -O3' P + [ impropers ] + C4 C8 N9 C1' + C5 N1 C6 O6 + C6 C2 N1 H1 + C2 H21 N2 H22 + N9 N7 C8 H8 + N2 N1 C2 N3 + +[ DG3 ] + [ atoms ] + P P 1.16590 1 + O1P O2 -0.77610 2 + O2P O2 -0.77610 3 + O5' OS -0.49540 4 + C5' CJ -0.00690 5 + H5'1 H1 0.07540 6 + H5'2 H1 0.07540 7 + C4' CT 0.16290 8 + H4' H1 0.11760 9 + O4' OS -0.36910 10 + C1' CT 0.03580 11 + H1' H2 0.17460 12 + N9 N* 0.05770 13 + C8 CK 0.07360 14 + H8 H5 0.19970 15 + N7 NB -0.57250 16 + C5 CB 0.19910 17 + C6 C 0.49180 18 + O6 O -0.56990 19 + N1 NA -0.50530 20 + H1 H 0.35200 21 + C2 CA 0.74320 22 + N2 N2 -0.92300 23 + H21 H 0.42350 24 + H22 H 0.42350 25 + N3 NC -0.66360 26 + C4 CB 0.18140 27 + C3' C7 0.07130 28 + H3' H1 0.09850 29 + C2' CT -0.08540 30 + H2'1 HC 0.07180 31 + H2'2 HC 0.07180 32 + O3' OH -0.65490 33 + H3T HO 0.43960 34 + [ bonds ] + P O1P + P O2P + P O5' + O5' C5' + C5' H5'1 + C5' H5'2 + C5' C4' + C4' H4' + C4' O4' + C4' C3' + O4' C1' + C1' H1' + C1' N9 + C1' C2' + N9 C8 + N9 C4 + C8 H8 + C8 N7 + N7 C5 + C5 C6 + C5 C4 + C6 O6 + C6 N1 + N1 H1 + N1 C2 + C2 N2 + C2 N3 + N2 H21 + N2 H22 + N3 C4 + C3' H3' + C3' C2' + C3' O3' + C2' H2'1 + C2' H2'2 + O3' H3T + -O3' P + [ impropers ] + C4 C8 N9 C1' + C5 N1 C6 O6 + C6 C2 N1 H1 + C2 H21 N2 H22 + N9 N7 C8 H8 + N2 N1 C2 N3 + +[ DGN ] + [ atoms ] + H5T HO 0.44220 1 + O5' OH -0.63180 2 + C5' CJ -0.00690 3 + H5'1 H1 0.07540 4 + H5'2 H1 0.07540 5 + C4' CT 0.16290 6 + H4' H1 0.11760 7 + O4' OS -0.36910 8 + C1' CT 0.03580 9 + H1' H2 0.17460 10 + N9 N* 0.05770 11 + C8 CK 0.07360 12 + H8 H5 0.19970 13 + N7 NB -0.57250 14 + C5 CB 0.19910 15 + C6 C 0.49180 16 + O6 O -0.56990 17 + N1 NA -0.50530 18 + H1 H 0.35200 19 + C2 CA 0.74320 20 + N2 N2 -0.92300 21 + H21 H 0.42350 22 + H22 H 0.42350 23 + N3 NC -0.66360 24 + C4 CB 0.18140 25 + C3' C7 0.07130 26 + H3' H1 0.09850 27 + C2' CT -0.08540 28 + H2'1 HC 0.07180 29 + H2'2 HC 0.07180 30 + O3' OH -0.65490 31 + H3T HO 0.43960 32 + [ bonds ] + H5T O5' + O5' C5' + C5' H5'1 + C5' H5'2 + C5' C4' + C4' H4' + C4' O4' + C4' C3' + O4' C1' + C1' H1' + C1' N9 + C1' C2' + N9 C8 + N9 C4 + C8 H8 + C8 N7 + N7 C5 + C5 C6 + C5 C4 + C6 O6 + C6 N1 + N1 H1 + N1 C2 + C2 N2 + C2 N3 + N2 H21 + N2 H22 + N3 C4 + C3' H3' + C3' C2' + C3' O3' + C2' H2'1 + C2' H2'2 + O3' H3T + [ impropers ] + C4 C8 N9 C1' + C5 N1 C6 O6 + C6 C2 N1 H1 + C2 H21 N2 H22 + N9 N7 C8 H8 + N2 N1 C2 N3 + +[ DC5 ] + [ atoms ] + H5T HO 0.44220 1 + O5' OH -0.63180 2 + C5' CJ -0.00690 3 + H5'1 H1 0.07540 4 + H5'2 H1 0.07540 5 + C4' CT 0.16290 6 + H4' H1 0.11760 7 + O4' OS -0.36910 8 + C1' CT -0.01160 9 + H1' H2 0.19630 10 + N1 N* -0.03390 11 + C6 C1 -0.01830 12 + H6 H4 0.22930 13 + C5 C1 -0.52220 14 + H5 HA 0.18630 15 + C4 CA 0.84390 16 + N4 N2 -0.97730 17 + H41 H 0.43140 18 + H42 H 0.43140 19 + N3 NC -0.77480 20 + C2 C 0.79590 21 + O2 O -0.65480 22 + C3' C7 0.07130 23 + H3' H1 0.09850 24 + C2' CT -0.08540 25 + H2'1 HC 0.07180 26 + H2'2 HC 0.07180 27 + O3' OS -0.52320 28 + [ bonds ] + H5T O5' + O5' C5' + C5' H5'1 + C5' H5'2 + C5' C4' + C4' H4' + C4' O4' + C4' C3' + O4' C1' + C1' H1' + C1' N1 + C1' C2' + N1 C6 + N1 C2 + C6 H6 + C6 C5 + C5 H5 + C5 C4 + C4 N4 + C4 N3 + N4 H41 + N4 H42 + N3 C2 + C2 O2 + C3' H3' + C3' C2' + C3' O3' + C2' H2'1 + C2' H2'2 + [ impropers ] + C1' N1 C6 C2 + N1 N3 C2 O2 + C4 H41 N4 H42 + N1 C5 C6 H6 + C6 C4 C5 H5 + C5 N4 C4 N3 + +[ DC ] + [ atoms ] + P P 1.16590 1 + O1P O2 -0.77610 2 + O2P O2 -0.77610 3 + O5' OS -0.49540 4 + C5' CJ -0.00690 5 + H5'1 H1 0.07540 6 + H5'2 H1 0.07540 7 + C4' CT 0.16290 8 + H4' H1 0.11760 9 + O4' OS -0.36910 10 + C1' CT -0.01160 11 + H1' H2 0.19630 12 + N1 N* -0.03390 13 + C6 C1 -0.01830 14 + H6 H4 0.22930 15 + C5 C1 -0.52220 16 + H5 HA 0.18630 17 + C4 CA 0.84390 18 + N4 N2 -0.97730 19 + H41 H 0.43140 20 + H42 H 0.43140 21 + N3 NC -0.77480 22 + C2 C 0.79590 23 + O2 O -0.65480 24 + C3' C7 0.07130 25 + H3' H1 0.09850 26 + C2' CT -0.08540 27 + H2'1 HC 0.07180 28 + H2'2 HC 0.07180 29 + O3' OS -0.52320 30 + [ bonds ] + P O1P + P O2P + P O5' + O5' C5' + C5' H5'1 + C5' H5'2 + C5' C4' + C4' H4' + C4' O4' + C4' C3' + O4' C1' + C1' H1' + C1' N1 + C1' C2' + N1 C6 + N1 C2 + C6 H6 + C6 C5 + C5 H5 + C5 C4 + C4 N4 + C4 N3 + N4 H41 + N4 H42 + N3 C2 + C2 O2 + C3' H3' + C3' C2' + C3' O3' + C2' H2'1 + C2' H2'2 + -O3' P + [ impropers ] + C2 C6 N1 C1' + N1 N3 C2 O2 + C4 H41 N4 H42 + N1 C5 C6 H6 + C6 C4 C5 H5 + C5 N4 C4 N3 + +[ DC3 ] + [ atoms ] + P P 1.16590 1 + O1P O2 -0.77610 2 + O2P O2 -0.77610 3 + O5' OS -0.49540 4 + C5' CJ -0.00690 5 + H5'1 H1 0.07540 6 + H5'2 H1 0.07540 7 + C4' CT 0.16290 8 + H4' H1 0.11760 9 + O4' OS -0.36910 10 + C1' CT -0.01160 11 + H1' H2 0.19630 12 + N1 N* -0.03390 13 + C6 C1 -0.01830 14 + H6 H4 0.22930 15 + C5 C1 -0.52220 16 + H5 HA 0.18630 17 + C4 CA 0.84390 18 + N4 N2 -0.97730 19 + H41 H 0.43140 20 + H42 H 0.43140 21 + N3 NC -0.77480 22 + C2 C 0.79590 23 + O2 O -0.65480 24 + C3' C7 0.07130 25 + H3' H1 0.09850 26 + C2' CT -0.08540 27 + H2'1 HC 0.07180 28 + H2'2 HC 0.07180 29 + O3' OH -0.65490 30 + H3T HO 0.43960 31 + [ bonds ] + P O1P + P O2P + P O5' + O5' C5' + C5' H5'1 + C5' H5'2 + C5' C4' + C4' H4' + C4' O4' + C4' C3' + O4' C1' + C1' H1' + C1' N1 + C1' C2' + N1 C6 + N1 C2 + C6 H6 + C6 C5 + C5 H5 + C5 C4 + C4 N4 + C4 N3 + N4 H41 + N4 H42 + N3 C2 + C2 O2 + C3' H3' + C3' C2' + C3' O3' + C2' H2'1 + C2' H2'2 + O3' H3T + -O3' P + [ impropers ] + C2 C6 N1 C1' + N1 N3 C2 O2 + C4 H41 N4 H42 + N1 C5 C6 H6 + C6 C4 C5 H5 + C5 N4 C4 N3 + +[ DCN ] + [ atoms ] + H5T HO 0.44220 1 + O5' OH -0.63180 2 + C5' CJ -0.00690 3 + H5'1 H1 0.07540 4 + H5'2 H1 0.07540 5 + C4' CT 0.16290 6 + H4' H1 0.11760 7 + O4' OS -0.36910 8 + C1' CT -0.01160 9 + H1' H2 0.19630 10 + N1 N* -0.03390 11 + C6 C1 -0.01830 12 + H6 H4 0.22930 13 + C5 C1 -0.52220 14 + H5 HA 0.18630 15 + C4 CA 0.84390 16 + N4 N2 -0.97730 17 + H41 H 0.43140 18 + H42 H 0.43140 19 + N3 NC -0.77480 20 + C2 C 0.79590 21 + O2 O -0.65480 22 + C3' C7 0.07130 23 + H3' H1 0.09850 24 + C2' CT -0.08540 25 + H2'1 HC 0.07180 26 + H2'2 HC 0.07180 27 + O3' OH -0.65490 28 + H3T HO 0.43960 29 + [ bonds ] + H5T O5' + O5' C5' + C5' H5'1 + C5' H5'2 + C5' C4' + C4' H4' + C4' O4' + C4' C3' + O4' C1' + C1' H1' + C1' N1 + C1' C2' + N1 C6 + N1 C2 + C6 H6 + C6 C5 + C5 H5 + C5 C4 + C4 N4 + C4 N3 + N4 H41 + N4 H42 + N3 C2 + C2 O2 + C3' H3' + C3' C2' + C3' O3' + C2' H2'1 + C2' H2'2 + O3' H3T + [ impropers ] + C2 C6 N1 C1' + N1 N3 C2 O2 + C4 H41 N4 H42 + N1 C5 C6 H6 + C6 C4 C5 H5 + N3 C4 N4 C5 + diff --git a/pmx/data/mutff45/amber14sb_OL15.ff/ffbonded.itp b/pmx/data/mutff45/amber14sb_OL15.ff/ffbonded.itp new file mode 100644 index 00000000..eb8830f9 --- /dev/null +++ b/pmx/data/mutff45/amber14sb_OL15.ff/ffbonded.itp @@ -0,0 +1,1573 @@ +[ bondtypes ] +; i j func b0 kb + C C 1 0.15250 259408.0 + C CA 1 0.14090 392459.2 + C CB 1 0.14190 374049.6 + C CM 1 0.14440 343088.0 + C CS 1 0.14440 343088.0 + C CT 1 0.15220 265265.6 + C CX 1 0.15220 265265.6 + C N 1 0.13350 410032.0 + C N* 1 0.13830 354803.2 + C NA 1 0.13880 349782.4 + C NC 1 0.13580 382417.6 + C O 1 0.12290 476976.0 + C O2 1 0.12500 548940.8 + C OH 1 0.13640 376560.0 + C OS 1 0.13230 376560.0 + C H4 1 0.10800 307105.6 + C H5 1 0.10800 307105.6 + CA CA 1 0.14000 392459.2 + CA CB 1 0.14040 392459.2 + CA CM 1 0.14330 357313.6 + CA CS 1 0.14330 357313.6 + CA CN 1 0.14000 392459.2 + CA CT 1 0.15100 265265.6 + CA HA 1 0.10800 307105.6 + CA H4 1 0.10800 307105.6 + CA N2 1 0.13400 402500.8 + CA NA 1 0.13810 357313.6 + CA NC 1 0.13390 404174.4 + CA OH 1 0.13640 376560.0 + CB CB 1 0.13700 435136.0 + CB N* 1 0.13740 364844.8 + CB NB 1 0.13910 346435.2 + CB NC 1 0.13540 385764.8 + CD HA 1 0.10800 307105.6 + CD CD 1 0.14000 392459.2 + CD CM 1 0.13500 459403.2 + CD CS 1 0.13500 459403.2 + CD CT 1 0.15100 265265.6 + CK H5 1 0.10800 307105.6 + CK N* 1 0.13710 368192.0 + CK NB 1 0.13040 442667.2 + CP H5 1 0.10800 307105.6 + CP N* 1 0.13710 368192.0 + CP NB 1 0.13040 442667.2 + CM CM 1 0.13500 459403.2 + CM CT 1 0.15100 265265.6 + CM HA 1 0.10800 307105.6 + CM H4 1 0.10800 307105.6 + CM H5 1 0.10800 307105.6 + CM N* 1 0.13650 374886.4 + CM OS 1 0.12400 401664.0 + CS CS 1 0.13500 459403.2 + CS CT 1 0.15100 265265.6 + CS HA 1 0.10800 307105.6 + CS H4 1 0.10800 307105.6 + CS H5 1 0.10800 307105.6 + CS N* 1 0.13650 374886.4 + CS OS 1 0.12400 401664.0 + CQ H5 1 0.10800 307105.6 + CQ NC 1 0.13240 420073.6 + CT CT 1 0.15260 259408.0 + CT C7 1 0.15260 259408.0 + CX CT 1 0.15260 259408.0 + CT HC 1 0.10900 284512.0 + CT H1 1 0.10900 284512.0 + C7 H1 1 0.10900 284512.0 + CX H1 1 0.10900 284512.0 + CT H2 1 0.10900 284512.0 + CT H3 1 0.10900 284512.0 + CT HP 1 0.10900 284512.0 + CX HP 1 0.10900 284512.0 + CT N* 1 0.14750 282001.6 + CT N2 1 0.14630 282001.6 + CT OH 1 0.14100 267776.0 + C7 OH 1 0.14100 267776.0 + CT OS 1 0.14100 267776.0 + C7 OS 1 0.14100 267776.0 + C* HC 1 0.10800 307105.6 + C* CB 1 0.14590 324678.4 + C* CT 1 0.14950 265265.6 + C* CW 1 0.13520 456892.8 + CB CN 1 0.14190 374049.6 + CC CT 1 0.15040 265265.6 + CC CV 1 0.13750 428441.6 + CC CW 1 0.13710 433462.4 + CC NA 1 0.13850 353129.6 + CC NB 1 0.13940 343088.0 + CN NA 1 0.13800 358150.4 + CR H5 1 0.10800 307105.6 + CR NA 1 0.13430 399153.6 + CR NB 1 0.13350 408358.4 + CT N 1 0.14490 282001.6 + CX N 1 0.14490 282001.6 + CT N3 1 0.14710 307105.6 + CX N3 1 0.14710 307105.6 + CT NT 1 0.14710 307105.6 + CT S 1 0.18100 189953.6 + CT SH 1 0.18100 198321.6 + CT CY 1 0.14580 334720.0 + CT CZ 1 0.14590 334720.0 + CV H4 1 0.10800 307105.6 + CV NB 1 0.13940 343088.0 + CW H4 1 0.10800 307105.6 + CW NA 1 0.13810 357313.6 + CY NY 1 0.11500 502080.0 + CZ CZ 1 0.12060 502080.0 + CZ HZ 1 0.10560 334720.0 + OP P 1 0.14800 439320.0 + O2 P 1 0.14800 439320.0 + OH P 1 0.16100 192464.0 + OS P 1 0.16100 192464.0 + NA P 1 0.18400 209200.0 + H N2 1 0.10100 363171.2 + H N* 1 0.10100 363171.2 + H NA 1 0.10100 363171.2 + H N 1 0.10100 363171.2 + H N3 1 0.10100 363171.2 + H NT 1 0.10100 363171.2 + HO OH 1 0.09600 462750.4 + HO OS 1 0.09600 462750.4 + HS SH 1 0.13360 229283.2 + S S 1 0.20380 138908.8 + F CT 1 0.13800 307105.6 + Cl CT 1 0.17660 194137.6 + Br CT 1 0.19440 133051.2 + I CT 1 0.21660 123846.4 + F CA 1 0.13590 323004.8 + Cl CA 1 0.17270 161502.4 + I CA 1 0.20750 143092.8 + Br CA 1 0.18900 143929.6 + EP O 1 0.02000 502080.0 + EP OH 1 0.02000 502080.0 + EP OS 1 0.02000 502080.0 + EP N3 1 0.02000 502080.0 + EP NT 1 0.02000 502080.0 + EP NB 1 0.02000 502080.0 + EP NC 1 0.02000 502080.0 + EP S 1 0.07000 502080.0 + EP SH 1 0.07000 502080.0 + CI H1 1 0.10900 284512.0 + CJ H1 1 0.10900 284512.0 + CI CT 1 0.15260 259408.0 + CJ CT 1 0.15260 259408.0 + OS CI 1 0.14100 267776.0 + OS CJ 1 0.14100 267776.0 + OH CI 1 0.14100 267776.0 + OH CJ 1 0.14100 267776.0 + C5 H5 1 0.10800 307105.6 + C2 H5 1 0.10800 307105.6 + C5 N* 1 0.13710 368192.0 + C2 N* 1 0.13710 368192.0 + C5 NB 1 0.13040 442667.2 + C2 NB 1 0.13040 442667.2 + C C4 1 0.14440 343088.0 + C C1 1 0.14440 343088.0 + CA C4 1 0.14330 357313.6 + CA C1 1 0.14330 357313.6 + C4 C4 1 0.13500 459403.2 + C1 C1 1 0.13500 459403.2 + C4 CT 1 0.15100 265265.6 + C1 CT 1 0.15100 265265.6 + C4 HA 1 0.10800 307105.6 + C1 HA 1 0.10800 307105.6 + C4 H4 1 0.10800 307105.6 + C1 H4 1 0.10800 307105.6 + C4 N* 1 0.13650 374886.4 + C1 N* 1 0.13650 374886.4 + C 2C 1 0.15220 265265.6 + C* 2C 1 0.14950 265265.6 + C8 C8 1 0.15260 259408.0 + C8 CX 1 0.15260 259408.0 + C8 H1 1 0.10900 284512.0 + C8 HC 1 0.10900 284512.0 + C8 HP 1 0.10900 284512.0 + C8 N2 1 0.14630 282001.6 + C8 N3 1 0.14710 307105.6 + CA 2C 1 0.15100 265265.6 + CC 2C 1 0.15040 265265.6 + CO O2 1 0.12500 548940.8 + CO 2C 1 0.15220 265265.6 + CT 2C 1 0.15260 259408.0 + CT 3C 1 0.15260 259408.0 + CX 2C 1 0.15260 259408.0 + CX 3C 1 0.15260 259408.0 + H1 2C 1 0.10900 284512.0 + H1 3C 1 0.10900 284512.0 + HC 2C 1 0.10900 284512.0 + HC 3C 1 0.10900 284512.0 + OH 2C 1 0.14100 267776.0 + OH 3C 1 0.14100 267776.0 + S 2C 1 0.18100 189953.6 + SH 2C 1 0.18100 198321.6 + 2C 2C 1 0.15260 259408.0 + 2C 3C 1 0.15260 259408.0 + + + +[ constrainttypes ] +; this section is implemented manually from bond & angle values + +; constraints for rigid CH3 groups + MCH3 CT 2 0.166426 + MCH3 S 2 0.193875 + MCH3 MCH3 2 0.092163 +; constraints for rigid NH3 groups + MNH3 CT 2 0.158254 + MNH3 MNH3 2 0.080229 + +; angle-derived constraints for OH and SH groups in proteins +; The constraint A-C is calculated from the angle A-B-C and bonds A-B, B-C. + C HO 2 0.195074 + CA HO 2 0.195074 + CT HO 2 0.194132 + CT HS 2 0.235935 + + +[ angletypes ] +; i j k func th0 cth +HW OW HW 1 104.520 836.800 ; TIP3P water +HW HW OW 1 127.740 0.000 ; (found in crystallographic water with 3 bonds) +C C O 1 120.000 669.440 +C C OH 1 120.000 669.440 +CA C CA 1 120.000 527.184 +CA C OH 1 120.000 585.760 +CA C OS 1 120.000 585.760 +CC NA P 1 125.100 641.826 +CR NA P 1 125.100 641.826 +NA P OP 1 102.380 358.987 +CB C NA 1 111.300 585.760 +CB C O 1 128.800 669.440 +CM C NA 1 114.100 585.760 +CM C O 1 125.300 669.440 +CS C NA 1 114.100 585.760 +CS C O 1 125.300 669.440 +CT C O 1 120.400 669.440 +CX C O 1 120.400 669.440 +CT C O2 1 117.000 585.760 +CX C O2 1 117.000 585.760 +CT C N 1 116.600 585.760 +CX C N 1 116.600 585.760 +CT C CT 1 117.000 527.184 +CT C OS 1 115.000 669.440 +CT C OH 1 110.000 669.440 +CX C OH 1 110.000 669.440 +N* C NA 1 115.400 585.760 +N* C NC 1 118.600 585.760 +N* C O 1 120.900 669.440 +NA C O 1 120.600 669.440 +NC C O 1 122.500 669.440 +N C O 1 122.900 669.440 +O C O 1 126.000 669.440 +O C OH 1 120.000 669.440 +O C OS 1 125.000 669.440 +O2 C O2 1 126.000 669.440 +H4 C C 1 120.000 418.400 +H4 C CM 1 115.000 418.400 +H4 C CS 1 115.000 418.400 +H4 C CT 1 115.000 418.400 +H4 C O 1 120.000 418.400 +H4 C OH 1 120.000 418.400 +H5 C N 1 120.000 418.400 +H5 C O 1 119.000 418.400 +H5 C OH 1 107.000 418.400 +H5 C OS 1 107.000 418.400 +C CA CA 1 120.000 527.184 +C CA HA 1 120.000 418.400 +CA CA CA 1 120.000 527.184 +CA CA CB 1 120.000 527.184 +CA CA CT 1 120.000 585.760 +CA CA HA 1 120.000 418.400 +CA CA H4 1 120.000 418.400 +CA CA OH 1 120.000 585.760 +CA CA CN 1 120.000 527.184 +CB CA HA 1 120.000 418.400 +CB CA H4 1 120.000 418.400 +CB CA N2 1 123.500 585.760 +CB CA NC 1 117.300 585.760 +CM CA N2 1 120.100 585.760 +CM CA NC 1 121.500 585.760 +CS CA N2 1 120.100 585.760 +CS CA NC 1 121.500 585.760 +CN CA HA 1 120.000 418.400 +NA CA NC 1 123.300 585.760 +N2 CA NA 1 116.000 585.760 +N2 CA NC 1 119.300 585.760 +N2 CA N2 1 120.000 585.760 +F CA CA 1 121.000 585.760 +Cl CA CA 1 118.800 585.760 +Br CA CA 1 118.800 585.760 +I CA CA 1 118.800 585.760 +C CB CB 1 119.200 527.184 +C CB NB 1 130.000 585.760 +CA CB CB 1 117.300 527.184 +CA CB NB 1 132.400 585.760 +CB CB N* 1 106.200 585.760 +CB CB NB 1 110.400 585.760 +CB CB NC 1 127.700 585.760 +C* CB CA 1 134.900 527.184 +C* CB CN 1 108.800 527.184 +CA CB CN 1 116.200 527.184 +N* CB NC 1 126.200 585.760 +CD CD CM 1 120.000 527.184 +CD CD CS 1 120.000 527.184 +CD CD CT 1 120.000 585.760 +CM CD CT 1 120.000 585.760 +CS CD CT 1 120.000 585.760 +HA CD HA 1 119.000 292.880 +HA CD CD 1 120.000 418.400 +HA CD CM 1 120.000 418.400 +HA CD CS 1 120.000 418.400 +H5 CK N* 1 123.050 418.400 +H5 CK NB 1 123.050 418.400 +N* CK NB 1 113.900 585.760 +H5 CP N* 1 123.050 418.400 +H5 CP NB 1 123.050 418.400 +N* CP NB 1 113.900 585.760 +C CM CM 1 120.700 527.184 +C CM CT 1 119.700 585.760 +C CM HA 1 119.700 418.400 +C CM H4 1 119.700 418.400 +CA CM CM 1 117.000 527.184 +CA CM HA 1 123.300 418.400 +CA CM H4 1 123.300 418.400 +CM CM CT 1 119.700 585.760 +CM CM HA 1 119.700 418.400 +CM CM H4 1 119.700 418.400 +CM CM N* 1 121.200 585.760 +CM CM OS 1 125.000 669.440 +H4 CM N* 1 119.100 418.400 +H4 CM OS 1 113.000 418.400 +HA CM HA 1 120.000 292.880 +HA CM CD 1 120.000 418.400 +HA CM CT 1 120.000 418.400 +C CS CS 1 120.700 527.184 +C CS CT 1 119.700 585.760 +C CS HA 1 119.700 418.400 +C CS H4 1 119.700 418.400 +CA CS CS 1 117.000 527.184 +CA CS HA 1 123.300 418.400 +CA CS H4 1 123.300 418.400 +CM CS CT 1 119.700 585.760 +CS CS HA 1 119.700 418.400 +CS CS H4 1 119.700 418.400 +CS CS N* 1 121.200 585.760 +CS CS OS 1 125.000 669.440 +H4 CS N* 1 119.100 418.400 +H4 CS OS 1 113.000 418.400 +HA CS HA 1 120.000 292.880 +HA CS CD 1 120.000 418.400 +HA CS CT 1 120.000 418.400 +NC CQ NC 1 129.100 585.760 +H5 CQ NC 1 115.450 418.400 +H1 CT H1 1 109.500 292.880 +H1 CX H1 1 109.500 292.880 +H1 CT N* 1 109.500 418.400 +H1 CT OH 1 109.500 418.400 +H1 C7 OH 1 109.500 418.400 +H1 CT OS 1 109.500 418.400 +H1 C7 OS 1 109.500 418.400 +H1 CT CM 1 109.500 418.400 +H1 CT CS 1 109.500 418.400 +H1 CT CY 1 110.000 418.400 +H1 CT CZ 1 110.000 418.400 +H1 CT N 1 109.500 418.400 +H1 CX N 1 109.500 418.400 +H1 CT S 1 109.500 418.400 +H1 CT SH 1 109.500 418.400 +H1 CT N2 1 109.500 418.400 +H1 CT NT 1 109.500 418.400 +H2 CT H2 1 109.500 292.880 +H2 CT N* 1 109.500 418.400 +H2 CT OS 1 109.500 418.400 +HP CT HP 1 109.500 292.880 +HP CX HP 1 109.500 292.880 +HP CT N3 1 109.500 418.400 +HP CX N3 1 109.500 418.400 +HC CT HC 1 109.500 292.880 +HC CT CM 1 109.500 418.400 +HC CT CS 1 109.500 418.400 +HC CT CD 1 109.500 418.400 +HC CT CZ 1 110.000 418.400 +C CT H1 1 109.500 418.400 +C CX H1 1 109.500 418.400 +C CT HP 1 109.500 418.400 +C CX HP 1 109.500 418.400 +C CT HC 1 109.500 418.400 +C CT N 1 110.100 527.184 +C CX N 1 110.100 527.184 +C CT N3 1 111.200 669.440 +C CX N3 1 111.200 669.440 +C CT CT 1 111.100 527.184 +C CT CX 1 111.100 527.184 +C CX CT 1 111.100 527.184 +C CT OS 1 109.500 502.080 +CA CT HC 1 109.500 418.400 +CC CT CT 1 113.100 527.184 +CC CT CX 1 113.100 527.184 +CC CT HC 1 109.500 418.400 +CM CT CT 1 111.000 527.184 +CM CT OS 1 109.500 418.400 +CS CT CT 1 111.000 527.184 +CS CT OS 1 109.500 418.400 +CT CT CT 1 109.500 334.720 +C7 CT CT 1 109.500 334.720 +CT C7 CT 1 109.500 334.720 +CT CT CX 1 109.500 334.720 +CT CT HC 1 109.500 418.400 +C7 CT HC 1 109.500 418.400 +CX CT HC 1 109.500 418.400 +CT CT H1 1 109.500 418.400 +C7 CT H1 1 109.500 418.400 +CT C7 H1 1 109.500 418.400 +CT CX H1 1 109.500 418.400 +CX CT H1 1 109.500 418.400 +CT CT H2 1 109.500 418.400 +CT CT HP 1 109.500 418.400 +CT CX HP 1 109.500 418.400 +CT CT N* 1 109.500 418.400 +CT CT OH 1 109.500 418.400 +C7 CT OH 1 109.500 418.400 +CT C7 OH 1 109.500 418.400 +CX CT OH 1 109.500 418.400 +CT CT OS 1 109.500 418.400 +C7 CT OS 1 109.500 418.400 +CT C7 OS 1 109.500 418.400 +CT CT S 1 114.700 418.400 +CX CT S 1 114.700 418.400 +CT CT SH 1 108.600 418.400 +CX CT SH 1 108.600 418.400 +CT CT CA 1 114.000 527.184 +CX CT CA 1 114.000 527.184 +CT CT N2 1 111.200 669.440 +CT CT N 1 109.700 669.440 +CT CX N 1 109.700 669.440 +CT CT N3 1 111.200 669.440 +CT CX N3 1 111.200 669.440 +CT CT NT 1 111.200 669.440 +CT CT CY 1 110.000 527.184 +CT CT CZ 1 110.000 527.184 +C* CT CT 1 115.600 527.184 +C* CT CX 1 115.600 527.184 +C* CT HC 1 109.500 418.400 +OS CT OS 1 101.000 1338.880 +OS CT CY 1 110.000 418.400 +OS CT CZ 1 110.000 418.400 +OS CT N* 1 109.500 418.400 +F CT F 1 109.100 644.336 +F CT H1 1 109.500 418.400 +F CT CT 1 109.000 418.400 +F CT H2 1 109.500 418.400 +Cl CT CT 1 108.500 418.400 +Cl CT H1 1 108.500 418.400 +Br CT CT 1 108.000 418.400 +Br CT H1 1 106.500 418.400 +I CT CT 1 106.000 418.400 +CT CC NA 1 120.000 585.760 +CT CC CV 1 120.000 585.760 +CT CC NB 1 120.000 585.760 +CV CC NA 1 120.000 585.760 +CW CC NA 1 120.000 585.760 +CW CC NB 1 120.000 585.760 +CT CC CW 1 120.000 585.760 +H5 CR NA 1 120.000 418.400 +H5 CR NB 1 120.000 418.400 +NA CR NA 1 120.000 585.760 +NA CR NB 1 120.000 585.760 +CC CV H4 1 120.000 418.400 +CC CV NB 1 120.000 585.760 +H4 CV NB 1 120.000 418.400 +CC CW H4 1 120.000 418.400 +CC CW NA 1 120.000 585.760 +C* CW H4 1 120.000 418.400 +C* CW NA 1 108.700 585.760 +H4 CW NA 1 120.000 418.400 +CB C* CT 1 128.600 585.760 +CB C* CW 1 106.400 527.184 +CT C* CW 1 125.000 585.760 +CA CN CB 1 122.700 527.184 +CA CN NA 1 132.800 585.760 +CB CN NA 1 104.400 585.760 +CT CY NY 1 180.000 669.440 +CT CZ CZ 1 180.000 669.440 +CZ CZ HZ 1 180.000 418.400 +C N CT 1 121.900 418.400 +C N CX 1 121.900 418.400 +C N H 1 120.000 418.400 +CT N H 1 118.040 418.400 +CX N H 1 118.040 418.400 +CT N CT 1 118.000 418.400 +CT N CX 1 118.000 418.400 +H N H 1 120.000 292.880 +C N* CM 1 121.600 585.760 +C N* CS 1 121.600 585.760 +C N* CT 1 117.600 585.760 +C N* H 1 119.200 418.400 +CB N* CK 1 105.400 585.760 +CB N* CP 1 105.400 585.760 +CB N* CT 1 125.800 585.760 +CB N* H 1 125.800 418.400 +CK N* CT 1 128.800 585.760 +CK N* H 1 128.800 418.400 +CP N* CT 1 128.800 585.760 +CP N* H 1 128.800 418.400 +CM N* CT 1 121.200 585.760 +CM N* H 1 121.200 418.400 +CS N* CT 1 121.200 585.760 +CS N* H 1 121.200 418.400 +CA N2 H 1 120.000 418.400 +CA N2 CT 1 123.200 418.400 +CT N2 H 1 118.400 418.400 +H N2 H 1 120.000 292.880 +CT N3 H 1 109.500 418.400 +CX N3 H 1 109.500 418.400 +CT N3 CT 1 109.500 418.400 +CT N3 CX 1 109.500 418.400 +H N3 H 1 109.500 292.880 +CT NT H 1 109.500 418.400 +CT NT CT 1 109.500 418.400 +H NT H 1 109.500 292.880 +C NA C 1 126.400 585.760 +C NA CA 1 125.200 585.760 +C NA H 1 116.800 418.400 +CA NA H 1 118.000 418.400 +CC NA CR 1 120.000 585.760 +CC NA H 1 120.000 418.400 +CR NA CW 1 120.000 585.760 +CR NA H 1 120.000 418.400 +CW NA H 1 120.000 418.400 +CN NA CW 1 111.600 585.760 +CN NA H 1 123.100 418.400 +CB NB CK 1 103.800 585.760 +CB NB CP 1 103.800 585.760 +CC NB CR 1 117.000 585.760 +CR NB CV 1 117.000 585.760 +C NC CA 1 120.500 585.760 +CA NC CB 1 112.200 585.760 +CA NC CQ 1 118.600 585.760 +CB NC CQ 1 111.000 585.760 +C OH HO 1 113.000 418.400 +CA OH HO 1 113.000 418.400 +CT OH HO 1 108.500 460.240 +C7 OH HO 1 108.500 460.240 +HO OH P 1 108.500 376.560 +C OS CT 1 117.000 502.080 +CM OS CT 1 117.000 502.080 +CS OS CT 1 117.000 502.080 +CT OS CT 1 109.500 502.080 +CT OS P 1 120.500 836.800 +C7 OS P 1 120.500 836.800 +C OS P 1 120.500 836.800 +P OS P 1 120.500 836.800 +O2 P OH 1 108.230 376.560 +O2 P O2 1 119.900 1171.520 +OP P OP 1 119.900 1171.520 +OP P OS 1 108.230 836.800 +O2 P OS 1 108.230 836.800 +OH P OS 1 102.600 376.560 +OS P OS 1 102.600 376.560 +CT S CT 1 98.900 518.816 +CT S S 1 103.700 569.024 +CT SH HS 1 96.000 359.824 +HS SH HS 1 92.070 292.880 +CB NB EP 1 126.000 1255.200 +CC NB EP 1 126.000 1255.200 +CK NB EP 1 126.000 1255.200 +CP NB EP 1 126.000 1255.200 +CR NB EP 1 126.000 1255.200 +CV NB EP 1 126.000 1255.200 +C NC EP 1 120.000 1255.200 +CA NC EP 1 120.000 1255.200 +CB NC EP 1 120.000 1255.200 +CQ NC EP 1 120.000 1255.200 +CT N3 EP 1 109.500 1255.200 +H N3 EP 1 109.500 1255.200 +CT NT EP 1 109.500 1255.200 +H NT EP 1 109.500 1255.200 +C O EP 1 120.000 1255.200 +EP O EP 1 120.000 1255.200 +C OH EP 1 120.000 1255.200 +CT OH EP 1 109.500 1255.200 +HO OH EP 1 109.500 1255.200 +EP OH EP 1 109.500 1255.200 +C OS EP 1 109.500 1255.200 +CM OS EP 1 109.500 1255.200 +CS OS EP 1 109.500 1255.200 +CT OS EP 1 109.500 1255.200 +EP OS EP 1 109.500 1255.200 +CT S EP 1 90.000 1255.200 +CT SH EP 1 90.000 1255.200 +P OS EP 1 109.500 1255.200 +EP S EP 1 180.000 1255.200 +EP SH EP 1 180.000 1255.200 +HS SH EP 1 90.000 1255.200 +H1 CI CT 1 109.500 418.400 +H1 CJ CT 1 109.500 418.400 +H1 CI H1 1 109.500 292.880 +H1 CJ H1 1 109.500 292.880 +CI CT H1 1 109.500 418.400 +CJ CT H1 1 109.500 418.400 +CI CT OS 1 109.500 418.400 +CJ CT OS 1 109.500 418.400 +CI CT CT 1 109.500 334.720 +CJ CT CT 1 109.500 334.720 +CJ CT C7 1 109.500 334.720 +OS CI H1 1 109.500 418.400 +OS CJ H1 1 109.500 418.400 +OS CI CT 1 109.500 418.400 +OS CJ CT 1 109.500 418.400 +P OS CI 1 120.500 836.800 +P OS CJ 1 120.500 836.800 +OH CI H1 1 109.500 418.400 +OH CJ H1 1 109.500 418.400 +OH CI CT 1 109.500 418.400 +OH CJ CT 1 109.500 418.400 +HO OH CI 1 108.500 460.240 +HO OH CJ 1 108.500 460.240 +H5 C5 N* 1 123.050 418.400 +H5 C2 N* 1 123.050 418.400 +H5 C5 NB 1 123.050 418.400 +H5 C2 NB 1 123.050 418.400 +N* C5 NB 1 113.900 585.760 +N* C2 NB 1 113.900 585.760 +CB N* C5 1 105.400 585.760 +CB N* C2 1 105.400 585.760 +C5 N* CT 1 128.800 585.760 +C2 N* CT 1 128.800 585.760 +CB NB C5 1 103.800 585.760 +CB NB C2 1 103.800 585.760 +C4 C NA 1 114.100 585.760 +C1 C NA 1 114.100 585.760 +C4 C O 1 125.300 669.440 +C1 C O 1 125.300 669.440 +C4 CA N2 1 120.100 585.760 +C1 CA N2 1 120.100 585.760 +C4 CA NC 1 121.500 585.760 +C1 CA NC 1 121.500 585.760 +C C4 C4 1 120.700 527.184 +C C1 C1 1 120.700 527.184 +C C4 CT 1 119.700 585.760 +C C1 CT 1 119.700 585.760 +C C4 HA 1 119.700 418.400 +C C1 HA 1 119.700 418.400 +C C4 H4 1 119.700 418.400 +C C1 H4 1 119.700 418.400 +CA C4 C4 1 117.000 527.184 +CA C1 C1 1 117.000 527.184 +CA C4 HA 1 123.300 418.400 +CA C1 HA 1 123.300 418.400 +CA C4 H4 1 123.300 418.400 +CA C1 H4 1 123.300 418.400 +C4 C4 CT 1 119.700 585.760 +C1 C1 CT 1 119.700 585.760 +C4 C4 HA 1 119.700 418.400 +C1 C1 HA 1 119.700 418.400 +C4 C4 H4 1 119.700 418.400 +C1 C1 H4 1 119.700 418.400 +C4 C4 N* 1 121.200 585.760 +C1 C1 N* 1 121.200 585.760 +H4 C4 N* 1 119.100 418.400 +H4 C1 N* 1 119.100 418.400 +H1 CT C4 1 109.500 418.400 +H1 CT C1 1 109.500 418.400 +HC CT C4 1 109.500 418.400 +HC CT C1 1 109.500 418.400 +C N* C4 1 121.600 585.760 +C N* C1 1 121.600 585.760 +C4 N* CT 1 121.200 585.760 +C1 N* CT 1 121.200 585.760 +EP S S 1 96.700 1255.200 +N C 2C 1 116.600 585.760 +O C 2C 1 120.400 669.440 +OH C 2C 1 110.000 669.440 +CB C* 2C 1 128.600 585.760 +CW C* 2C 1 125.000 585.760 +C8 C8 C8 1 109.500 334.720 +C8 C8 CX 1 109.500 334.720 +C8 C8 H1 1 109.500 418.400 +C8 C8 HC 1 109.500 418.400 +C8 C8 HP 1 109.500 418.400 +C8 C8 N2 1 111.200 669.440 +C8 C8 N3 1 111.200 669.440 +CX C8 HC 1 109.500 418.400 +H1 C8 H1 1 109.500 292.880 +H1 C8 N2 1 109.500 418.400 +HC C8 HC 1 109.500 292.880 +HP C8 HP 1 109.500 292.880 +HP C8 N3 1 109.500 418.400 +CA CA 2C 1 120.000 585.760 +CV CC 2C 1 120.000 585.760 +CW CC 2C 1 120.000 585.760 +NA CC 2C 1 120.000 585.760 +NB CC 2C 1 120.000 585.760 +O2 CO O2 1 126.000 669.440 +O2 CO 2C 1 117.000 585.760 +HC CT 2C 1 109.500 418.400 +HC CT 3C 1 109.500 418.400 +C CX C8 1 111.100 527.184 +C CX 2C 1 111.100 527.184 +C CX 3C 1 111.100 527.184 +C8 CX H1 1 109.500 418.400 +C8 CX N 1 109.700 669.440 +C8 CX N3 1 111.200 669.440 +H1 CX 2C 1 109.500 418.400 +H1 CX 3C 1 109.500 418.400 +HP CX C8 1 109.500 418.400 +HP CX 2C 1 109.500 418.400 +HP CX 3C 1 109.500 418.400 +N CX 2C 1 109.700 669.440 +N CX 3C 1 109.700 669.440 +N3 CX 2C 1 111.200 669.440 +N3 CX 3C 1 111.200 669.440 +C8 N2 CA 1 123.200 418.400 +C8 N2 H 1 118.400 418.400 +C8 N3 H 1 109.500 418.400 +HO OH 2C 1 108.500 460.240 +HO OH 3C 1 108.500 460.240 +CT S 2C 1 98.900 518.816 +2C S S 1 103.700 569.024 +HS SH 2C 1 96.000 359.824 +C 2C CX 1 111.100 527.184 +C 2C HC 1 109.500 418.400 +C 2C 2C 1 111.100 527.184 +C* 2C CX 1 115.600 527.184 +C* 2C HC 1 109.500 418.400 +CA 2C CX 1 114.000 527.184 +CA 2C HC 1 109.500 418.400 +CC 2C CX 1 113.100 527.184 +CC 2C HC 1 109.500 418.400 +CO 2C CX 1 111.100 527.184 +CO 2C HC 1 109.500 418.400 +CO 2C 2C 1 111.100 527.184 +CT 2C HC 1 109.500 418.400 +CT 2C 3C 1 109.500 334.720 +CX 2C H1 1 109.500 418.400 +CX 2C HC 1 109.500 418.400 +CX 2C OH 1 109.500 418.400 +CX 2C S 1 114.700 418.400 +CX 2C SH 1 108.600 418.400 +CX 2C 2C 1 109.500 334.720 +CX 2C 3C 1 109.500 334.720 +H1 2C H1 1 109.500 292.880 +H1 2C OH 1 109.500 418.400 +H1 2C S 1 109.500 418.400 +H1 2C SH 1 109.500 418.400 +H1 2C 2C 1 109.500 418.400 +HC 2C HC 1 109.500 292.880 +HC 2C 2C 1 109.500 418.400 +HC 2C 3C 1 109.500 418.400 +S 2C 2C 1 114.700 418.400 +CT 3C CT 1 109.500 334.720 +CT 3C CX 1 109.500 334.720 +CT 3C H1 1 109.500 418.400 +CT 3C HC 1 109.500 418.400 +CT 3C OH 1 109.500 418.400 +CT 3C 2C 1 109.500 334.720 +CX 3C H1 1 109.500 418.400 +CX 3C HC 1 109.500 418.400 +CX 3C OH 1 109.500 418.400 +CX 3C 2C 1 109.500 334.720 +H1 3C OH 1 109.500 418.400 +HC 3C 2C 1 109.500 418.400 + + +[ dihedraltypes ] ; improper +;i j k l func phase kd pn +Br CA CA CA 4 180.00 4.60240 2 +C4 N2 CA NC 4 180.00 4.60240 2 +C1 N2 CA NC 4 180.00 4.60240 2 +CA CA CA 2C 4 180.00 4.60240 2 +CA CA CA CT 4 180.00 4.60240 2 +CA CA CA OH 4 180.00 4.60240 2 +CA CA C OH 4 180.00 4.60240 2 +CA CA C OS 4 180.00 4.60240 2 +CB C5 N* CT 4 180.00 4.18400 2 +CB C2 N* CT 4 180.00 4.18400 2 +CB CK N* CT 4 180.00 4.18400 2 +CB CP N* CT 4 180.00 4.18400 2 +CB CT C* CW 4 180.00 4.60240 2 +CB N2 CA NC 4 180.00 4.60240 2 +C C4 C4 CT 4 180.00 4.60240 2 +C C1 C1 CT 4 180.00 4.60240 2 +C C4 N* CT 4 180.00 4.18400 2 +C C1 N* CT 4 180.00 4.18400 2 +C CM CM CT 4 180.00 4.60240 2 +C CM N* CT 4 180.00 4.18400 2 +C CS CM CT 4 180.00 4.60240 2 +C CS CS CT 4 180.00 4.60240 2 +C CS N* CT 4 180.00 4.18400 2 +C CT N H 4 180.00 4.60240 2 +C CT N O 4 180.00 4.60240 2 +C CX N H 4 180.00 4.60240 2 +CM CT CM HA 4 180.00 4.60240 2 +CM H4 C O 4 180.00 4.60240 2 +CM N2 CA NC 4 180.00 4.60240 2 +CR CC NA P 4 180.00 4.60240 2 +CS CT CS HA 4 180.00 4.60240 2 +CS H4 C O 4 180.00 4.60240 2 +CS N2 CA NC 4 180.00 4.60240 2 +CT CV CC NA 4 180.00 4.60240 2 +CT CW CC NA 4 180.00 4.60240 2 +NB CW CC CT 4 180.00 4.60240 2 +CT CW CC NB 4 180.00 4.60240 2 +CT O C OH 4 180.00 43.93200 2 +H5 O C OH 4 180.00 4.60240 2 +H5 O C OS 4 180.00 4.60240 2 +N2 NA CA NC 4 180.00 4.60240 2 +2C O C OH 4 180.00 43.93200 2 +X CT N CT 4 180.00 4.18400 2 +X CT N CX 4 180.00 4.18400 2 +X N2 CA N2 4 180.00 43.93200 2 +X O2 C O2 4 180.00 43.93200 2 +X O2 CO O2 4 180.00 43.93200 2 +X X C4 H4 4 180.00 4.60240 2 +X X C1 H4 4 180.00 4.60240 2 +X X C4 HA 4 180.00 4.60240 2 +X X C1 HA 4 180.00 4.60240 2 +X X C5 H5 4 180.00 4.60240 2 +X X C2 H5 4 180.00 4.60240 2 +X X CA H4 4 180.00 4.60240 2 +X X CA H5 4 180.00 4.60240 2 +X X CA HA 4 180.00 4.60240 2 +X X CK H5 4 180.00 4.60240 2 +X X CM H4 4 180.00 4.60240 2 +X X CM HA 4 180.00 4.60240 2 +X X C O 4 180.00 43.93200 2 +X X CP H5 4 180.00 4.60240 2 +X X CQ H5 4 180.00 4.60240 2 +X X CR H5 4 180.00 4.60240 2 +X X CS H4 4 180.00 4.60240 2 +X X CS HA 4 180.00 4.60240 2 +X X CV H4 4 180.00 4.60240 2 +X X CW H4 4 180.00 4.60240 2 +X X N2 H 4 180.00 4.18400 2 +X X NA H 4 180.00 4.18400 2 +X X N H 4 180.00 4.18400 2 + + +[ dihedraltypes ] +;i j k l func + 3C CX N C 9 0.0 0.00000 4 + 3C CX N C 9 0.0 3.34720 3 + 3C CX N C 9 0.0 7.53120 2 + 3C CX N C 9 0.0 8.36800 1 + 2C 2C S CT 9 0.0 0.23849 4 + 2C 2C S CT 9 0.0 1.73218 3 + 2C 2C S CT 9 0.0 1.84933 2 + 2C 2C S CT 9 180.0 1.03345 1 + 2C 2C CO O2 9 0.0 0.26778 4 + 2C 2C CO O2 9 180.0 1.63176 2 + 2C 2C C OH 9 180.0 0.27614 4 + 2C 2C C OH 9 180.0 0.10460 3 + 2C 2C C OH 9 180.0 4.61914 2 + 2C 2C C OH 9 180.0 3.44762 1 + 2C 2C C O 9 0.0 0.00000 1 + 2C 2C C N 9 0.0 0.17573 4 + 2C 2C C N 9 180.0 0.35564 3 + 2C 2C C N 9 180.0 3.53548 2 + 2C 2C C N 9 180.0 2.54806 1 + 2C S S 2C 9 0.0 1.58574 4 + 2C S S 2C 9 0.0 2.85349 3 + 2C S S 2C 9 0.0 18.74432 2 + 2C S S 2C 9 0.0 1.75728 1 + 2C CX N C 9 0.0 0.00000 4 + 2C CX N C 9 0.0 3.34720 3 + 2C CX N C 9 0.0 7.53120 2 + 2C CX N C 9 0.0 8.36800 1 + 2C C OH HO 9 0.0 0.47279 4 + 2C C OH HO 9 0.0 2.00414 3 + 2C C OH HO 9 180.0 11.32190 2 + 2C C OH HO 9 180.0 1.87443 1 + 2C CC NB CR 9 180.0 10.04160 2 + 2C CC NA H 9 180.0 5.85760 2 + 2C CC NA CR 9 180.0 5.85760 2 + 2C CC CW NA 9 180.0 22.48900 2 + 2C CC CW H4 9 180.0 22.48900 2 + 2C CC CV NB 9 180.0 21.54760 2 + 2C CC CV H4 9 180.0 21.54760 2 + CX 3C OH HO 9 0.0 0.05439 4 + CX 3C OH HO 9 0.0 0.98742 3 + CX 3C OH HO 9 0.0 1.05018 2 + CX 3C OH HO 9 180.0 0.02510 1 + CX 3C 2C CT 9 0.0 0.96232 4 + CX 3C 2C CT 9 0.0 0.44769 3 + CX 3C 2C CT 9 0.0 0.22175 2 + CX 3C 2C CT 9 0.0 1.87025 1 + CT 3C OH HO 9 0.0 0.20083 4 + CT 3C OH HO 9 0.0 0.98742 3 + CT 3C OH HO 9 180.0 0.33054 2 + CT 3C OH HO 9 0.0 2.69031 1 + CT 3C 2C CT 9 0.0 0.93722 4 + CT 3C 2C CT 9 0.0 0.44769 3 + CT 3C 2C CT 9 180.0 0.32217 2 + CT 3C 2C CT 9 0.0 0.84517 1 + HC 2C 3C HC 9 0.0 0.62760 3 + HC 2C 3C CX 9 0.0 0.66944 3 + HC 2C 3C CT 9 0.0 0.66944 3 + HC 2C 2C HC 9 0.0 0.62760 3 + CX 2C S S 9 180.0 0.56484 4 + CX 2C S S 9 0.0 1.26357 3 + CX 2C S S 9 0.0 2.78654 2 + CX 2C S S 9 0.0 0.23430 1 + CX 2C SH HS 9 0.0 0.12552 4 + CX 2C SH HS 9 0.0 1.05437 3 + CX 2C SH HS 9 0.0 2.56061 2 + CX 2C SH HS 9 0.0 0.38493 1 + CX 2C OH HO 9 0.0 0.02929 4 + CX 2C OH HO 9 0.0 1.11713 3 + CX 2C OH HO 9 0.0 1.85770 2 + CX 2C OH HO 9 0.0 0.88282 1 + CX 2C CO O2 9 180.0 0.12970 4 + CX 2C CO O2 9 0.0 0.00000 3 + CX 2C CO O2 9 180.0 3.21750 2 + CX 2C CO O2 9 0.0 0.00000 1 + CX 2C C OH 9 180.0 0.83262 4 + CX 2C C OH 9 0.0 0.03347 3 + CX 2C C OH 9 180.0 2.40580 2 + CX 2C C OH 9 180.0 5.01662 1 + CX 2C C O 9 0.0 0.00000 1 + CX 2C C N 9 0.0 0.03347 4 + CX 2C C N 9 180.0 1.25938 3 + CX 2C C N 9 180.0 2.02924 2 + CX 2C C N 9 180.0 3.46435 1 + CX 2C 3C HC 9 0.0 0.66944 3 + CX 2C 3C CT 9 0.0 0.74894 4 + CX 2C 3C CT 9 0.0 0.59413 3 + CX 2C 3C CT 9 180.0 0.11297 2 + CX 2C 3C CT 9 0.0 1.58574 1 + CX 2C 2C S 9 0.0 0.11715 4 + CX 2C 2C S 9 0.0 0.06694 3 + CX 2C 2C S 9 0.0 1.02508 2 + CX 2C 2C S 9 0.0 1.74473 1 + CX 2C 2C HC 9 0.0 0.66944 3 + CX 2C 2C CO 9 180.0 0.23430 4 + CX 2C 2C CO 9 180.0 2.54387 3 + CX 2C 2C CO 9 180.0 0.92885 2 + CX 2C 2C CO 9 180.0 5.71953 1 + CX 2C 2C C 9 0.0 0.57739 4 + CX 2C 2C C 9 180.0 1.72381 3 + CX 2C 2C C 9 0.0 0.34727 2 + CX 2C 2C C 9 180.0 0.82006 1 + CT 2C 3C HC 9 0.0 0.66944 3 + OS P OS CT 9 0.0 1.04600 3 + OS P OS CT 9 0.0 5.02080 2 + OS P OS C7 9 358.284 1.23039 3 + OS P OS C7 9 350.882 4.07626 2 + OS P OS C7 9 1.7533 3.88639 1 + OS P OS CI 9 357.2474 1.48473 3 + OS P OS CI 9 351.9596 5.25733 2 + OS P OS CI 9 31.7951 0.77480 1 + OS P OS CJ 9 357.2474 1.48473 3 + OS P OS CJ 9 351.9596 5.25733 2 + OS P OS CJ 9 31.7951 0.77480 1 + OS CT N* CS 9 16.0 1.47059 4 + OS CT N* CS 9 179.3 2.43300 3 + OS CT N* CS 9 16.8 7.31681 2 + OS CT N* CS 9 149.9 4.28919 1 + OS CT N* CP 9 4.0 1.07119 4 + OS CT N* CP 9 168.7 1.85238 3 + OS CT N* CP 9 6.2 4.45788 2 + OS CT N* CP 9 74.8 2.95014 1 + OS CT N* CM 9 85.3644 0.57618 4 + OS CT N* CM 9 178.4857 2.83495 3 + OS CT N* CM 9 357.8520 9.11325 2 + OS CT N* CM 9 94.7062 2.35655 1 + OS CT N* CK 9 175.4175 2.78500 3 + OS CT N* CK 9 350.8842 7.65295 2 + OS CT N* CK 9 43.9252 4.96444 1 + OS CT N* C5 9 19.1 1.29357 4 + OS CT N* C5 9 171.6 1.91435 3 + OS CT N* C5 9 15.6 4.49374 2 + OS CT N* C5 9 68.8 4.04011 1 + OS CT N* C2 9 179.0053 2.40095 3 + OS CT N* C2 9 356.5233 7.65103 2 + OS CT N* C2 9 40.5923 6.46424 1 + OS CT N* C4 9 32.2 1.29842 4 + OS CT N* C4 9 185.9 3.92237 3 + OS CT N* C4 9 16.5 6.83912 2 + OS CT N* C4 9 147.0 5.12565 1 + OS CT N* C1 9 94.1874 1.08776 4 + OS CT N* C1 9 170.6402 5.04666 3 + OS CT N* C1 9 360.2802 8.95573 2 + OS CT N* C1 9 111.3175 2.87663 1 + OS CT CT OS 9 0.0 0.60250 3 + OS CT CT OS 9 0.0 4.91620 2 + OS C7 CT OS 9 0.0 0.60250 3 + OS C7 CT OS 9 0.0 4.91620 2 + OS CT CT OH 9 0.0 0.60250 3 + OS CT CT OH 9 0.0 4.91620 2 + OS CT C7 OH 9 0.0 0.60250 3 + OS CT C7 OH 9 0.0 4.91620 2 + OS C7 CT OH 9 0.0 0.60250 3 + OS C7 CT OH 9 0.0 4.91620 2 + OP P OS CA 9 180.0 0.00000 1 + OH P OS CT 9 0.0 1.04600 3 + OH P OS CT 9 0.0 5.02080 2 + OH P OS C7 9 358.284 1.23039 3 + OH P OS C7 9 350.882 4.07626 2 + OH P OS C7 9 1.7533 3.88639 1 + OH P OS CI 9 357.2474 1.48473 3 + OH P OS CI 9 351.9596 5.25733 2 + OH P OS CI 9 31.7951 0.77480 1 + OH P OS CJ 9 357.2474 1.48473 3 + OH P OS CJ 9 351.9596 5.25733 2 + OH P OS CJ 9 31.7951 0.77480 1 + OH CT CT OH 9 0.0 0.60250 3 + OH CT CT OH 9 0.0 4.91620 2 + OH CT C7 OH 9 0.0 0.60250 3 + OH CT C7 OH 9 0.0 4.91620 2 + OH C 2C HC 9 0.0 0.00000 2 + O C OS CT 9 180.0 11.29680 2 + O C OS CT 9 180.0 5.85760 1 + O C N EP 9 180.0 0.00000 2 + O C 2C HC 9 180.0 0.33472 3 + O C 2C HC 9 0.0 0.00000 2 + O C 2C HC 9 0.0 3.34720 1 + O2 CO 2C HC 9 0.0 0.00000 2 + N CX CT CC 9 0.0 0.37238 4 + N CX CT CC 9 0.0 0.91630 3 + N CX CT CC 9 180.0 0.92466 2 + N CX CT CC 9 180.0 1.28030 1 + N CX CT CA 9 180.0 0.02929 4 + N CX CT CA 9 0.0 0.80333 3 + N CX CT CA 9 180.0 1.21336 2 + N CX CT CA 9 180.0 0.05021 1 + N CX CT C* 9 0.0 0.12970 4 + N CX CT C* 9 0.0 0.97906 3 + N CX CT C* 9 180.0 1.30959 2 + N CX CT C* 9 0.0 0.33054 1 + N CX C N 9 0.0 0.00000 4 + N CX C N 9 180.0 2.30120 3 + N CX C N 9 180.0 6.61072 2 + N CX C N 9 180.0 1.88280 1 + N CX C8 C8 9 0.0 0.65084 3 + N CX 3C OH 9 0.0 0.39748 4 + N CX 3C OH 9 0.0 1.31796 3 + N CX 3C OH 9 0.0 0.02510 2 + N CX 3C OH 9 0.0 2.82002 1 + N CX 3C CT 9 180.0 0.00418 4 + N CX 3C CT 9 0.0 0.61923 3 + N CX 3C CT 9 180.0 0.90374 2 + N CX 3C CT 9 0.0 1.41001 1 + N CX 3C 2C 9 180.0 0.40585 4 + N CX 3C 2C 9 0.0 0.47279 3 + N CX 3C 2C 9 180.0 0.60250 2 + N CX 3C 2C 9 0.0 1.29704 1 + N CX 2C SH 9 0.0 0.13807 4 + N CX 2C SH 9 0.0 1.05018 3 + N CX 2C SH 9 180.0 2.03342 2 + N CX 2C SH 9 0.0 0.64434 1 + N CX 2C S 9 0.0 0.26778 4 + N CX 2C S 9 0.0 1.35143 3 + N CX 2C S 9 180.0 0.08786 2 + N CX 2C S 9 0.0 1.96230 1 + N CX 2C OH 9 0.0 0.66944 4 + N CX 2C OH 9 0.0 1.67778 3 + N CX 2C OH 9 180.0 1.02926 2 + N CX 2C OH 9 0.0 2.78654 1 + N CX 2C CO 9 0.0 0.37238 4 + N CX 2C CO 9 0.0 0.24267 3 + N CX 2C CO 9 180.0 2.70705 2 + N CX 2C CO 9 180.0 9.01234 1 + N CX 2C C 9 0.0 0.24686 4 + N CX 2C C 9 0.0 0.13807 3 + N CX 2C C 9 180.0 1.24265 2 + N CX 2C C 9 180.0 2.87859 1 + N CX 2C 3C 9 0.0 0.30543 4 + N CX 2C 3C 9 0.0 0.60250 3 + N CX 2C 3C 9 180.0 1.08366 2 + N CX 2C 3C 9 0.0 0.41003 1 + N CX 2C 2C 9 0.0 0.32635 4 + N CX 2C 2C 9 0.0 0.60250 3 + N CX 2C 2C 9 180.0 0.76986 2 + N CX 2C 2C 9 180.0 0.41840 1 + N CT CT OH 9 0.0 0.00000 4 + N CT CT OH 9 0.0 0.65270 3 + N CT CT OH 9 0.0 6.23416 2 + N CT CT OH 9 0.0 0.00000 1 + N C CX CT 9 0.0 0.00000 4 + N C CX CT 9 0.0 1.67360 3 + N C CX CT 9 0.0 0.83680 2 + N C CX CT 9 0.0 0.83680 1 + N C CX C8 9 0.0 0.00000 4 + N C CX C8 9 0.0 1.67360 3 + N C CX C8 9 0.0 0.83680 2 + N C CX C8 9 0.0 0.83680 1 + N C CX 3C 9 0.0 0.00000 4 + N C CX 3C 9 0.0 1.67360 3 + N C CX 3C 9 0.0 0.83680 2 + N C CX 3C 9 0.0 0.83680 1 + N C CX 2C 9 0.0 0.00000 4 + N C CX 2C 9 0.0 1.67360 3 + N C CX 2C 9 0.0 0.83680 2 + N C CX 2C 9 0.0 0.83680 1 + N C 2C HC 9 0.0 0.00000 2 + NB CC 2C HC 9 0.0 0.00000 2 + NA CC 2C HC 9 0.0 0.00000 2 + N3 CX CT CC 9 0.0 0.37238 4 + N3 CX CT CC 9 0.0 0.91630 3 + N3 CX CT CC 9 180.0 0.92466 2 + N3 CX CT CC 9 180.0 1.28030 1 + N3 CX CT CA 9 180.0 0.02929 4 + N3 CX CT CA 9 0.0 0.80333 3 + N3 CX CT CA 9 180.0 1.21336 2 + N3 CX CT CA 9 180.0 0.05021 1 + N3 CX CT C* 9 0.0 0.12970 4 + N3 CX CT C* 9 0.0 0.97906 3 + N3 CX CT C* 9 180.0 1.30959 2 + N3 CX CT C* 9 0.0 0.33054 1 + N3 CX C8 C8 9 0.0 0.65084 3 + N3 CX 3C OH 9 0.0 0.39748 4 + N3 CX 3C OH 9 0.0 1.31796 3 + N3 CX 3C OH 9 0.0 0.02510 2 + N3 CX 3C OH 9 0.0 2.82002 1 + N3 CX 3C CT 9 180.0 0.00418 4 + N3 CX 3C CT 9 0.0 0.61923 3 + N3 CX 3C CT 9 180.0 0.90374 2 + N3 CX 3C CT 9 0.0 1.41001 1 + N3 CX 3C 2C 9 180.0 0.40585 4 + N3 CX 3C 2C 9 0.0 0.47279 3 + N3 CX 3C 2C 9 180.0 0.60250 2 + N3 CX 3C 2C 9 0.0 1.29704 1 + N3 CX 2C SH 9 0.0 0.13807 4 + N3 CX 2C SH 9 0.0 1.05018 3 + N3 CX 2C SH 9 180.0 2.03342 2 + N3 CX 2C SH 9 0.0 0.64434 1 + N3 CX 2C S 9 0.0 0.26778 4 + N3 CX 2C S 9 0.0 1.35143 3 + N3 CX 2C S 9 180.0 0.08786 2 + N3 CX 2C S 9 0.0 1.96230 1 + N3 CX 2C OH 9 0.0 0.66944 4 + N3 CX 2C OH 9 0.0 1.67778 3 + N3 CX 2C OH 9 180.0 1.02926 2 + N3 CX 2C OH 9 0.0 2.78654 1 + N3 CX 2C CO 9 0.0 0.37238 4 + N3 CX 2C CO 9 0.0 0.24267 3 + N3 CX 2C CO 9 180.0 2.70705 2 + N3 CX 2C CO 9 180.0 9.01234 1 + N3 CX 2C C 9 0.0 0.24686 4 + N3 CX 2C C 9 0.0 0.13807 3 + N3 CX 2C C 9 180.0 1.24265 2 + N3 CX 2C C 9 180.0 2.87859 1 + N3 CX 2C 3C 9 0.0 0.30543 4 + N3 CX 2C 3C 9 0.0 0.60250 3 + N3 CX 2C 3C 9 180.0 1.08366 2 + N3 CX 2C 3C 9 0.0 0.41003 1 + N3 CX 2C 2C 9 0.0 0.32635 4 + N3 CX 2C 2C 9 0.0 0.60250 3 + N3 CX 2C 2C 9 180.0 0.76986 2 + N3 CX 2C 2C 9 180.0 0.41840 1 + HO OH CT CX 9 0.0 0.66944 3 + HO OH CT CX 9 0.0 1.04600 1 + HO OH CT CT 9 0.0 0.66944 3 + HO OH CT CT 9 0.0 1.04600 1 + HO OH C7 CT 9 0.0 0.66944 3 + HO OH C7 CT 9 0.0 1.04600 1 + HO OH C O 9 180.0 9.62320 2 + HO OH C O 9 0.0 7.94960 1 + HO OH 3C H1 9 0.0 0.69733 3 + HO OH 2C H1 9 0.0 0.69733 3 + H N C O 9 180.0 10.46000 2 + H N C O 9 0.0 8.36800 1 + H N2 CA N2 9 0.0 0.00000 4 + H N2 CA N2 9 180.0 10.04160 2 + HC CT CT OS 9 0.0 0.00000 3 + HC CT CT OS 9 0.0 1.04600 1 + HC CT C7 OS 9 0.0 0.00000 3 + HC CT C7 OS 9 0.0 1.04600 1 + HC CT CT OH 9 0.0 0.00000 3 + HC CT CT OH 9 0.0 1.04600 1 + HC CT C7 OH 9 0.0 0.00000 3 + HC CT C7 OH 9 0.0 1.04600 1 + HC CT CT HC 9 0.0 0.62760 3 + HC CT CT F 9 0.0 0.00000 3 + HC CT CT F 9 0.0 0.79496 1 + HC CT CT CX 9 0.0 0.66944 3 + HC CT CT CT 9 0.0 0.66944 3 + HC CT C7 CT 9 0.0 0.66944 3 + HC CT CT Cl 9 0.0 0.00000 3 + HC CT CT Cl 9 0.0 1.04600 1 + HC CT CT Br 9 0.0 0.00000 3 + HC CT CT Br 9 0.0 2.30120 1 + HC CT CS CS 9 180.0 1.58992 3 + HC CT CS CS 9 0.0 4.81160 1 + HC CT C O 9 180.0 0.33472 3 + HC CT C O 9 0.0 0.00000 2 + HC CT C O 9 0.0 3.34720 1 + HC CT CM CM 9 180.0 1.58992 3 + HC CT CM CM 9 0.0 4.81160 1 + HC CT C4 C4 9 180.0 1.58992 3 + HC CT C4 C4 9 0.0 4.81160 1 + HC CT C1 C1 9 180.0 1.58992 3 + HC CT C1 C1 9 0.0 4.81160 1 + HC CT 3C OH 9 0.0 0.00000 3 + HC CT 3C OH 9 0.0 1.04600 1 + HC CT 3C HC 9 0.0 0.62760 3 + HC CT 3C H1 9 0.0 0.65084 3 + HC CT 3C CX 9 0.0 0.66944 3 + HC CT 3C CT 9 0.0 0.66944 3 + HC CT 3C 2C 9 0.0 0.66944 3 + HC CT 2C HC 9 0.0 0.62760 3 + HC CT 2C 3C 9 0.0 0.66944 3 + HC C8 C8 HC 9 0.0 0.62760 3 + HA CS OS EP 9 180.0 0.00000 2 + HA CM OS EP 9 180.0 0.00000 2 + H4 CS OS EP 9 180.0 0.00000 2 + H4 CM OS EP 9 180.0 0.00000 2 + H2 CT OS EP 9 0.0 0.00000 3 + H1 CX CT OH 9 0.0 0.00000 3 + H1 CX CT OH 9 0.0 1.04600 1 + H1 CX C O 9 180.0 0.33472 3 + H1 CX C O 9 0.0 0.00000 2 + H1 CX C O 9 0.0 3.34720 1 + H1 CX 3C OH 9 0.0 0.00000 3 + H1 CX 3C OH 9 0.0 1.04600 1 + H1 CX 2C OH 9 0.0 0.00000 3 + H1 CX 2C OH 9 0.0 1.04600 1 + H1 CT S 2C 9 0.0 1.39467 3 + H1 CT OS EP 9 0.0 0.00000 3 + H1 CT OH EP 9 0.0 0.00000 3 + H1 CT NT EP 9 0.0 0.00000 3 + H1 CT CT OS 9 0.0 0.00000 3 + H1 CT CT OS 9 0.0 1.04600 1 + H1 CT C7 OS 9 0.0 0.00000 3 + H1 CT C7 OS 9 0.0 1.04600 1 + H1 C7 CT OS 9 0.0 0.00000 3 + H1 C7 CT OS 9 0.0 1.04600 1 + H1 CT CT OH 9 0.0 0.00000 3 + H1 CT CT OH 9 0.0 1.04600 1 + H1 C7 CT OH 9 0.0 0.00000 3 + H1 C7 CT OH 9 0.0 1.04600 1 + H1 CT C7 OH 9 0.0 0.00000 3 + H1 CT C7 OH 9 0.0 1.04600 1 + C7 OS P O2 9 358.284 1.23039 3 + CT C7 OS P 9 162.4967 0.63844 3 + CT C7 OS P 9 139.8342 2.70625 2 + CT C7 OS P 9 205.9192 7.42610 1 + CT CJ OS P 9 215.181 0.33372 4 + CT CJ OS P 9 181.607 2.24068 3 + CT CJ OS P 9 223.648 0.38189 2 + CT CJ OS P 9 23.578 2.20894 1 + H1 CT CT F 9 0.0 0.00000 3 + H1 CT CT F 9 0.0 0.79496 1 + H1 CT CT Cl 9 0.0 0.00000 3 + H1 CT CT Cl 9 0.0 1.04600 1 + H1 CT CT Br 9 0.0 0.00000 3 + H1 CT CT Br 9 0.0 2.30120 1 + H1 CT C O 9 180.0 0.33472 3 + H1 CT C O 9 0.0 0.00000 2 + H1 CT C O 9 0.0 3.34720 1 + H1 CT CI OS 9 0.0 1.04600 1 + H1 CT CJ OS 9 0.0 1.04600 1 + H1 CT CI OH 9 0.0 1.04600 1 + H1 CT CJ OH 9 0.0 1.04600 1 + H1 CI CT OS 9 0.0 1.04600 1 + H1 CJ CT OS 9 0.0 1.04600 1 + H1 CI CT OH 9 0.0 1.04600 1 + H1 CJ CT OH 9 0.0 1.04600 1 + F CT CT F 9 0.0 0.00000 3 + F CT CT F 9 180.0 5.02080 1 + EP S S EP 9 0.0 0.00000 3 + EP S S CT 9 0.0 0.00000 3 + EP S S 2C 9 0.0 0.00000 3 + CX CT CT CT 9 0.0 0.75312 3 + CX CT CT CT 9 180.0 1.04600 2 + CX CT CT CT 9 180.0 0.83680 1 + CX CT C N 9 0.0 0.00000 4 + CX CT C N 9 0.0 1.67360 3 + CX CT C N 9 0.0 0.83680 2 + CX CT C N 9 0.0 0.83680 1 + CX CT C* CW 9 0.0 0.00000 4 + CX CT C* CW 9 0.0 0.00000 3 + CX CT C* CW 9 0.0 0.00000 2 + CX CT C* CW 9 0.0 0.00000 1 + CX CT CC NB 9 180.0 0.19665 4 + CX CT CC NB 9 0.0 3.09616 3 + CX CT CC NB 9 0.0 0.85354 2 + CX CT CC NB 9 0.0 2.88696 1 + CX CT CC NA 9 180.0 0.15481 4 + CX CT CC NA 9 0.0 2.87022 3 + CX CT CC NA 9 180.0 1.64013 2 + CX CT CC NA 9 180.0 0.66944 1 + CX CT CC CV 9 180.0 0.04184 4 + CX CT CC CV 9 180.0 0.51045 3 + CX CT CC CV 9 0.0 3.13800 2 + CX CT CC CV 9 180.0 2.82002 1 + CX CT C* CB 9 180.0 0.39748 4 + CX CT C* CB 9 0.0 3.42670 3 + CX CT C* CB 9 0.0 1.70707 2 + CX CT C* CB 9 0.0 1.52716 1 + CX CT CA CA 9 180.0 0.20083 4 + CX CT CA CA 9 0.0 0.00000 3 + CX CT CA CA 9 180.0 0.28870 2 + CX CT CA CA 9 0.0 0.00000 1 + CX C8 C8 HC 9 0.0 0.66944 3 + CX C8 C8 C8 9 0.0 0.75312 3 + CX C8 C8 C8 9 180.0 1.04600 2 + CX C8 C8 C8 9 180.0 0.83680 1 + CW CC 2C HC 9 0.0 0.00000 2 + CW C* 2C HC 9 0.0 0.00000 2 + CV CC 2C HC 9 0.0 0.00000 2 + CT S S CT 9 0.0 2.51040 3 + CT S S CT 9 0.0 14.64400 2 + CT OS CT OS 9 0.0 0.41840 3 + CT OS CT OS 9 180.0 3.55640 2 + CT OS CT OS 9 180.0 5.64840 1 + CT OS CT N* 9 0.0 1.60247 3 + CT OS CT N* 9 0.0 2.71960 2 + CT OS CT CI 9 0.0 1.60247 3 + CT OS CT CI 9 180.0 0.41840 2 + CT OS CT CJ 9 0.0 1.60247 3 + CT OS CT CJ 9 180.0 0.41840 2 + CT CZ CZ HZ 9 0.0 0.00000 1 + CT CX N C 9 0.0 0.00000 4 + CT CX N C 9 0.0 3.34720 3 + CT CX N C 9 0.0 7.53120 2 + CT CX N C 9 0.0 8.36800 1 + CT CT OS EP 9 0.0 0.00000 3 + CT CT OS CT 9 0.0 1.60247 3 + CT CT OS CT 9 180.0 0.41840 2 + C7 CT OS CT 9 0.0 1.60247 3 + C7 CT OS CT 9 180.0 0.41840 2 + CT CT OS C 9 0.0 1.60247 3 + CT CT OS C 9 180.0 3.34720 1 + CT CT OH EP 9 0.0 0.00000 3 + CT CT NT EP 9 0.0 0.00000 3 + CT CT NT CT 9 0.0 1.25520 3 + CT CT NT CT 9 180.0 2.00832 2 + CT CT N C 9 0.0 0.00000 4 + CT CT N C 9 0.0 1.67360 3 + CT CT N C 9 0.0 8.36800 2 + CT CT N C 9 0.0 8.36800 1 + CT CT CT CT 9 0.0 0.75312 3 + CT CT CT CT 9 180.0 1.04600 2 + CT CT CT CT 9 180.0 0.83680 1 + CT CT C7 CT 9 0.0 0.75312 3 + CT CT C7 CT 9 180.0 1.04600 2 + CT CT C7 CT 9 180.0 0.83680 1 + CT CT C N 9 0.0 0.00000 4 + CT CT C N 9 0.0 1.67360 3 + CT CT C N 9 0.0 0.83680 2 + CT CT C N 9 0.0 0.83680 1 + CT CT CI OS 9 348.0953 4.02848 3 + CT CT CI OS 9 295.6327 0.38535 2 + CT CT CI OS 9 190.9765 4.92892 1 + CT CT CJ OS 9 348.0953 4.02848 3 + CT CT CJ OS 9 295.6327 0.38535 2 + CT CT CJ OS 9 190.9765 4.92892 1 + C7 CT CJ OS 9 348.0954 4.02848 3 + C7 CT CJ OS 9 295.6328 0.38535 2 + C7 CT CJ OS 9 190.9765 4.92892 1 + CT CT CI OH 9 348.0953 4.02848 3 + CT CT CI OH 9 295.6327 0.38535 2 + CT CT CI OH 9 190.9765 4.92892 1 + CT CT CJ OH 9 348.0954 4.02848 3 + CT CT CJ OH 9 295.6328 0.38535 2 + CT CT CJ OH 9 190.9765 4.92892 1 + C7 CT CJ OH 9 348.0953 4.02848 3 + C7 CT CJ OH 9 295.6327 0.38535 2 + C7 CT CJ OH 9 190.9765 4.92892 1 + CT CS CS CT 9 180.0 27.82360 2 + CT CS CS CT 9 180.0 7.94960 1 + CT C N EP 9 180.0 0.00000 2 + CT CM CM CT 9 180.0 27.82360 2 + CT CM CM CT 9 180.0 7.94960 1 + CS CS OS EP 9 180.0 0.00000 2 + CS CS C O 9 0.0 1.25520 3 + CS CS C O 9 180.0 9.10020 2 + CR NA P OP 9 0.0 0.00000 3 + C N CX C 9 0.0 0.00000 4 + C N CX C 9 0.0 1.75728 3 + C N CX C 9 0.0 1.12968 2 + C N CX C 9 0.0 0.00000 1 + CM CM OS EP 9 180.0 0.00000 2 + CM CM C O 9 0.0 1.25520 3 + CM CM C O 9 180.0 9.10020 2 + Cl CT CT Cl 9 0.0 0.00000 3 + Cl CT CT Cl 9 180.0 1.88280 1 + CI CT CT CT 9 0.0 0.75312 3 + CI CT CT CT 9 180.0 1.04600 2 + CI CT CT CT 9 180.0 0.83680 1 + CJ CT C7 CT 9 0.0 0.75312 3 + CJ CT C7 CT 9 180.0 1.04600 2 + CJ CT C7 CT 9 180.0 0.83680 1 + C CX CT CC 9 0.0 0.10460 4 + C CX CT CC 9 0.0 0.91630 3 + C CX CT CC 9 180.0 1.02090 2 + C CX CT CC 9 180.0 0.59831 1 + C CX CT CA 9 180.0 0.05021 4 + C CX CT CA 9 0.0 0.80333 3 + C CX CT CA 9 180.0 1.96230 2 + C CX CT CA 9 0.0 0.23012 1 + C CX CT C* 9 0.0 0.30962 4 + C CX CT C* 9 0.0 0.97906 3 + C CX CT C* 9 180.0 1.47695 2 + C CX CT C* 9 180.0 0.07113 1 + C CX C8 C8 9 0.0 0.65084 3 + C CX 3C OH 9 0.0 0.65270 4 + C CX 3C OH 9 0.0 1.31796 3 + C CX 3C OH 9 180.0 0.49790 2 + C CX 3C OH 9 180.0 2.91625 1 + C CX 3C CT 9 0.0 0.46861 4 + C CX 3C CT 9 0.0 0.61923 3 + C CX 3C CT 9 180.0 1.20918 2 + C CX 3C CT 9 180.0 1.69870 1 + C CX 3C 2C 9 0.0 0.48116 4 + C CX 3C 2C 9 0.0 0.47279 3 + C CX 3C 2C 9 180.0 3.07524 2 + C CX 3C 2C 9 0.0 0.67781 1 + C CX 2C SH 9 0.0 0.31380 4 + C CX 2C SH 9 0.0 1.05018 3 + C CX 2C SH 9 180.0 1.41001 2 + C CX 2C SH 9 180.0 1.12550 1 + C CX 2C S 9 0.0 1.16315 4 + C CX 2C S 9 0.0 1.35143 3 + C CX 2C S 9 180.0 1.64850 2 + C CX 2C S 9 0.0 2.51877 1 + C CX 2C OH 9 0.0 0.53974 4 + C CX 2C OH 9 0.0 1.67778 3 + C CX 2C OH 9 180.0 0.91211 2 + C CX 2C OH 9 180.0 2.76562 1 + C CX 2C CO 9 0.0 0.64434 4 + C CX 2C CO 9 0.0 0.24267 3 + C CX 2C CO 9 180.0 1.92046 2 + C CX 2C CO 9 0.0 1.77402 1 + C CX 2C C 9 0.0 0.44769 4 + C CX 2C C 9 0.0 0.13807 3 + C CX 2C C 9 180.0 1.26775 2 + C CX 2C C 9 0.0 4.37646 1 + C CX 2C 3C 9 0.0 0.79496 4 + C CX 2C 3C 9 0.0 0.60250 3 + C CX 2C 3C 9 180.0 2.59408 2 + C CX 2C 3C 9 0.0 2.95390 1 + C CX 2C 2C 9 0.0 0.60668 4 + C CX 2C 2C 9 0.0 0.60250 3 + C CX 2C 2C 9 180.0 1.64431 2 + C CX 2C 2C 9 180.0 1.76146 1 + CC NA P OP 9 0.0 1.00416 3 + CB C* 2C HC 9 0.0 0.00000 2 + CA C OH HO 9 0.0 0.27196 4 + CA C OH HO 9 0.0 0.00000 3 + CA C OH HO 9 180.0 3.69447 2 + CA C OH HO 9 0.0 0.00000 1 + C8 N2 CA N2 9 0.0 0.00000 4 + C8 N2 CA N2 9 180.0 10.04160 2 + C8 CX N C 9 0.0 0.00000 4 + C8 CX N C 9 0.0 3.34720 3 + C8 CX N C 9 0.0 7.53120 2 + C8 CX N C 9 0.0 8.36800 1 + C8 C8 N3 H 9 0.0 0.65084 3 + C8 C8 N2 CA 9 0.0 0.00000 1 + C8 C8 C8 N3 9 0.0 0.65084 3 + C8 C8 C8 N2 9 0.0 0.65084 3 + C8 C8 C8 HC 9 0.0 0.66944 3 + C8 C8 C8 C8 9 0.0 0.75312 3 + C8 C8 C8 C8 9 180.0 1.04600 2 + C8 C8 C8 C8 9 180.0 0.83680 1 + C4 C4 C O 9 0.0 1.25520 3 + C4 C4 C O 9 180.0 9.10020 2 + C1 C1 C O 9 0.0 1.25520 3 + C1 C1 C O 9 180.0 9.10020 2 + Br CT CT Br 9 0.0 0.00000 3 + Br CT CT Br 9 180.0 0.00000 1 + X 2C 2C X 9 0.0 0.65084 3 + X SH 2C X 9 0.0 1.04600 3 + X S 2C X 9 0.0 1.39467 3 + X OS P X 9 0.0 1.04600 3 + X OH P X 9 0.0 1.04600 3 + X CX N X 9 0.0 0.00000 2 + X CX N3 X 9 0.0 0.65084 3 + X CX 3C X 9 0.0 0.65084 3 + X CX 2C X 9 0.0 0.65084 3 + X CW NA X 9 180.0 6.27600 2 + X CV NB X 9 180.0 10.04160 2 + X CT S X 9 0.0 1.39467 3 + X CT SH X 9 0.0 1.04600 3 + X CT OS X 9 0.0 1.60387 3 + X C7 OS X 9 0.0 1.60387 3 + X CT OH X 9 0.0 0.69733 3 + X C7 OH X 9 0.0 0.69733 3 + X CT N* X 9 0.0 0.00000 2 + X CT N X 9 0.0 0.00000 2 + X CT NT X 9 0.0 1.25520 3 + X CT N3 X 9 0.0 0.65084 3 + X CT N2 X 9 0.0 0.00000 3 + X CT CZ X 9 0.0 0.00000 1 + X CT CY X 9 0.0 0.00000 1 + X CT CX X 9 0.0 0.65084 3 + X CT CT X 9 0.0 0.65084 3 + X C7 CT X 9 0.0 0.65084 3 + X CS OS X 9 180.0 4.39320 2 + X CS N* X 9 180.0 7.74040 2 + X CS CT X 9 0.0 0.00000 3 + X CS CS X 9 180.0 27.82360 2 + X CR NB X 9 180.0 20.92000 2 + X CR NA X 9 180.0 9.72780 2 + X CQ NC X 9 180.0 28.45120 2 + X CP N* X 9 180.0 7.11280 2 + X CP NB X 9 180.0 41.84000 2 + X C O X 9 180.0 11.71520 2 + X C OS X 9 180.0 11.29680 2 + X C OH X 9 180.0 9.62320 2 + X C N* X 9 180.0 6.06680 2 + X C N X 9 180.0 10.46000 2 + X CN NA X 9 180.0 6.38060 2 + X C NC X 9 180.0 16.73600 2 + X C NA X 9 180.0 5.64840 2 + X CM OS X 9 180.0 4.39320 2 + X CM N* X 9 180.0 7.74040 2 + X CM CT X 9 0.0 0.00000 3 + X CM CM X 9 180.0 27.82360 2 + X CK N* X 9 180.0 7.11280 2 + X CK NB X 9 180.0 41.84000 2 + X CI OS X 9 0.0 1.60387 3 + X CJ OS X 9 0.0 1.60387 3 + X CI OH X 9 0.0 0.69733 3 + X CJ OH X 9 0.0 0.69733 3 + X CI CT X 9 0.0 0.65084 3 + X CJ CT X 9 0.0 0.65084 3 + X CD CT X 9 0.0 0.00000 2 + X CD CS X 9 180.0 27.82360 2 + X CD CM X 9 180.0 27.82360 2 + X CD CD X 9 180.0 4.18400 2 + X C CX X 9 0.0 0.00000 2 + X C C X 9 180.0 15.16700 2 + X C* CW X 9 180.0 27.30060 2 + X C* CT X 9 0.0 0.00000 2 + X C CT X 9 0.0 0.00000 2 + X C CS X 9 180.0 9.10020 2 + X CC NB X 9 180.0 10.04160 2 + X CC NA X 9 180.0 5.85760 2 + X C CM X 9 180.0 9.10020 2 + X CC CW X 9 180.0 22.48900 2 + X CC CV X 9 180.0 21.54760 2 + X CC CT X 9 0.0 0.00000 2 + X C* CB X 9 180.0 7.00820 2 + X C CB X 9 180.0 12.55200 2 + X C CA X 9 180.0 15.16700 2 + X C C4 X 9 180.0 9.10020 2 + X C C1 X 9 180.0 9.10020 2 + X CB N* X 9 180.0 6.90360 2 + X CB NC X 9 180.0 17.36360 2 + X CB NB X 9 180.0 10.66920 2 + X CB CN X 9 180.0 12.55200 2 + X CB CB X 9 180.0 22.80280 2 + X CA OH X 9 180.0 3.76560 2 + X CA NC X 9 180.0 20.08320 2 + X CA NA X 9 180.0 6.27600 2 + X CA N2 X 9 180.0 10.04160 2 + X CA CT X 9 0.0 0.00000 2 + X CA CS X 9 180.0 10.66920 2 + X CA CN X 9 180.0 15.16700 2 + X CA CM X 9 180.0 10.66920 2 + X CA CB X 9 180.0 14.64400 2 + X CA CA X 9 180.0 15.16700 2 + X CA C4 X 9 180.0 10.66920 2 + X CA C1 X 9 180.0 10.66920 2 + X CA 2C X 9 0.0 0.00000 2 + X C8 N3 X 9 0.0 0.65084 3 + X C8 N2 X 9 0.0 0.00000 3 + X C8 CX X 9 0.0 0.65084 3 + X C8 C8 X 9 0.0 0.65084 3 + X C5 N* X 9 180.0 7.11280 2 + X C2 N* X 9 180.0 7.11280 2 + X C5 NB X 9 180.0 41.84000 2 + X C2 NB X 9 180.0 41.84000 2 + X C4 N* X 9 180.0 7.74040 2 + X C1 N* X 9 180.0 7.74040 2 + X C4 CT X 9 0.0 0.00000 3 + X C1 CT X 9 0.0 0.00000 3 + X C4 C4 X 9 180.0 27.82360 2 + X C1 C1 X 9 180.0 27.82360 2 diff --git a/pmx/data/mutff45/amber14sb_OL15.ff/ffnonbonded.itp b/pmx/data/mutff45/amber14sb_OL15.ff/ffnonbonded.itp new file mode 100644 index 00000000..bbd33e70 --- /dev/null +++ b/pmx/data/mutff45/amber14sb_OL15.ff/ffnonbonded.itp @@ -0,0 +1,97 @@ +[ atomtypes ] +; name at.num mass charge ptype sigma epsilon +Br 35 79.90 0.0000 A 4.64693e-01 2.45414e-01 +C 6 12.01 0.0000 A 3.39967e-01 3.59824e-01 +CA 6 12.01 0.0000 A 3.39967e-01 3.59824e-01 +CB 6 12.01 0.0000 A 3.39967e-01 3.59824e-01 +CC 6 12.01 0.0000 A 3.39967e-01 3.59824e-01 +CD 6 12.01 0.0000 A 3.39967e-01 3.59824e-01 +CI 6 12.01 0.0000 A 3.39967e-01 4.57730e-01 +CJ 6 12.01 0.0000 A 3.39967e-01 4.57730e-01 +C7 6 12.01 0.0000 A 3.39967e-01 4.57730e-01 +CK 6 12.01 0.0000 A 3.39967e-01 3.59824e-01 +C2 6 12.01 0.0000 A 3.39967e-01 3.59824e-01 +CM 6 12.01 0.0000 A 3.39967e-01 3.59824e-01 +C1 6 12.01 0.0000 A 3.39967e-01 3.59824e-01 +CN 6 12.01 0.0000 A 3.39967e-01 3.59824e-01 +CO 6 12.01 0.0000 A 3.39967e-01 3.59824e-01 +CP 6 12.01 0.0000 A 3.39967e-01 3.59824e-01 +CQ 6 12.01 0.0000 A 3.39967e-01 3.59824e-01 +CR 6 12.01 0.0000 A 3.39967e-01 3.59824e-01 +CS 6 12.01 0.0000 A 3.39967e-01 3.59824e-01 +CT 6 12.01 0.0000 A 3.39967e-01 4.57730e-01 +CV 6 12.01 0.0000 A 3.39967e-01 3.59824e-01 +CW 6 12.01 0.0000 A 3.39967e-01 3.59824e-01 +CX 6 12.01 0.0000 A 3.39967e-01 4.57730e-01 +CY 6 12.01 0.0000 A 3.39967e-01 3.59824e-01 +CZ 6 12.01 0.0000 A 3.39967e-01 3.59824e-01 +C* 6 12.01 0.0000 A 3.39967e-01 3.59824e-01 +C4 6 12.01 0.0000 A 3.39967e-01 3.59824e-01 +C5 6 12.01 0.0000 A 3.39967e-01 3.59824e-01 +C8 6 12.01 0.0000 A 3.39967e-01 4.57730e-01 +2C 6 12.01 0.0000 A 3.39967e-01 4.57730e-01 +3C 6 12.01 0.0000 A 3.39967e-01 4.57730e-01 +C0 20 40.08 0.0000 A 3.05240e-01 1.92376e+00 +F 9 19.00 0.0000 A 4.10348e-01 1.40750e-02 +H 1 1.008 0.0000 A 1.06908e-01 6.56888e-02 +HO 1 1.008 0.0000 A 0.00000e+00 0.00000e+00 +HS 1 1.008 0.0000 A 1.06908e-01 6.56888e-02 +HC 1 1.008 0.0000 A 2.64953e-01 6.56888e-02 +H1 1 1.008 0.0000 A 2.47135e-01 6.56888e-02 +H2 1 1.008 0.0000 A 2.29317e-01 6.56888e-02 +H3 1 1.008 0.0000 A 2.11499e-01 6.56888e-02 +HP 1 1.008 0.0000 A 1.95998e-01 6.56888e-02 +HA 1 1.008 0.0000 A 2.59964e-01 6.27600e-02 +H4 1 1.008 0.0000 A 2.51055e-01 6.27600e-02 +H5 1 1.008 0.0000 A 2.42146e-01 6.27600e-02 +HW 1 1.008 0.0000 A 0.00000e+00 0.00000e+00 +HZ 1 1.008 0.0000 A 2.59964e-01 6.27600e-02 +I 53 126.9 0.0000 A 5.09595e-01 2.24604e-01 +Cl 17 35.45 0.0000 A 4.47766e-01 1.48913e-01 +Na 11 22.99 0.0000 A 2.43928e-01 3.65846e-01 +MG 12 24.305 0.0000 A 2.76980e-01 1.23428e-02 +N 7 14.01 0.0000 A 3.25000e-01 7.11280e-01 +NA 7 14.01 0.0000 A 3.25000e-01 7.11280e-01 +NB 7 14.01 0.0000 A 3.25000e-01 7.11280e-01 +NC 7 14.01 0.0000 A 3.25000e-01 7.11280e-01 +N2 7 14.01 0.0000 A 3.25000e-01 7.11280e-01 +N3 7 14.01 0.0000 A 3.25000e-01 7.11280e-01 +NT 7 14.01 0.0000 A 3.25000e-01 7.11280e-01 +N* 7 14.01 0.0000 A 3.25000e-01 7.11280e-01 +NY 7 14.01 0.0000 A 3.25000e-01 7.11280e-01 +O 8 16.00 0.0000 A 2.95992e-01 8.78640e-01 +O2 8 16.00 0.0000 A 2.95992e-01 8.78640e-01 +OW 8 16.00 0.0000 A 3.15061e-01 6.36386e-01 +OH 8 16.00 0.0000 A 3.06647e-01 8.80314e-01 +OS 8 16.00 0.0000 A 3.00001e-01 7.11280e-01 +OP 8 16.00 0.0000 A 3.29633e-01 7.11280e-01 +P 15 30.97 0.0000 A 3.74177e-01 8.36800e-01 +S 16 32.06 0.0000 A 3.56359e-01 1.04600e+00 +SH 16 32.06 0.0000 A 3.56359e-01 1.04600e+00 +CU 29 63.55 0.0000 A 3.39967e-01 3.59824e-01 +FE 26 55.00 0.0000 A 0.00000e+00 0.00000e+00 +K 19 39.10 0.0000 A 3.03797e-01 8.10369e-01 +Rb 37 85.47 0.0000 A 3.23040e-01 1.37161e+00 +Cs 55 132.91 0.0000 A 3.52084e-01 1.70096e+00 +IB 0 131.0 0.0000 A 8.90899e-01 4.18400e-01 +EP 0 0.00 0.0000 A 0.00000e+00 0.00000e+00 +; spc water - use only with spc.itp & settles +OW_spc 8 15.9994 0.0000 A 3.16557e-01 6.50629e-01 +HW_spc 1 1.0080 0.0000 A 0.00000e+00 0.00000e+00 +Li 3 6.94 0.0000 A 1.82634e-01 1.17108e-01 +Zn 30 65.4 0.0000 A 1.95998e-01 5.23000e-02 +;tip4p-EW +HW_tip4pew 1 1.008 0.0000 A 0.00000e+00 0.00000e+00 +OW_tip4pew 8 16.00 0.0000 A 3.16435e-01 6.80946e-01 +; tip4p +HW_tip4p 1 1.008 0.0000 A 0.00000e+00 0.00000e+00 +OW_tip4p 8 16.00 0.0000 A 3.15365e-01 6.48520e-01 +;tip5p +HW_tip5p 1 1.008 0.0000 A 0.00000e+00 0.00000e+00 +OW_tip5p 8 16.00 0.0000 A 3.12000e-01 6.69440e-01 +; dummy defs +; MW=Dummy mass for tip4p/EW/5p water extra point charge +MW 0 0.0000 0.0000 A 0.00000e+00 0.00000e+00 +; Dummy masses for rigid CH3 and NH3 groups +MCH3 0 0.0000 0.0000 A 0.00000e+00 0.00000e+00 +MNH3 0 0.0000 0.0000 A 0.00000e+00 0.00000e+00 diff --git a/pmx/data/mutff45/amber14sb_OL15.ff/forcefield.doc b/pmx/data/mutff45/amber14sb_OL15.ff/forcefield.doc new file mode 100644 index 00000000..4ee4547d --- /dev/null +++ b/pmx/data/mutff45/amber14sb_OL15.ff/forcefield.doc @@ -0,0 +1,18 @@ +OL15 nucleic (Zgarbova et al., J. Chem. Theory Comput. 11, 5723-36, 2015; +J. Chem. Theory Comput. 7, 2886-2902, 2011) + +******************************************************************** +* OL15 nucleic acid package combines several dihedral * +* modifications, which were added to ff99 + bsc0 force field: * +* DNA: * +* chiOL4 (Krepl et al. 8, 2506-2520, 2012) * +* epsilon/zetaOL1 (Zgarbova et al. JCTC 9, 2339-2354, 2013) * +* betaOL1 (Zgarbova et al. JCTC 11, 5723-36, 2015) * +* RNA: * +* chiOL3 (Zgarbova et al. JCTC 7, 2866-2902, 2011) * +* see also http://ffol.upol.cz * +* OL15 nucleic is combined with amber14SB protein FF implemented * +* by Man Hoang Viet, vhman@ncsu.edu * +* In this version corrected typo in Na+ ion parameters * +******************************************************************** + diff --git a/pmx/data/mutff45/amber14sb_OL15.ff/forcefield.itp b/pmx/data/mutff45/amber14sb_OL15.ff/forcefield.itp new file mode 100644 index 00000000..13d9c850 --- /dev/null +++ b/pmx/data/mutff45/amber14sb_OL15.ff/forcefield.itp @@ -0,0 +1,23 @@ +******************************************************************** +* The original ffamber ports were written by Eric J. Sorin, * +* CSU Long Beach, Dept. of Chem & Biochem, and have now been * +* integrated with the standard gromacs distribution. * +* (Please don't blame Eric for errors we might have introduced.) * +* For the implementation/validation, please read/cite: * +* Sorin & Pande (2005). Biophys. J. 88(4), 2472-2493. * +* For related material and updates, please consult * +* http://chemistry.csulb.edu/ffamber/ * +******************************************************************** + +#define _FF_AMBER +#define _FF_AMBER12SB + +[ defaults ] +; nbfunc comb-rule gen-pairs fudgeLJ fudgeQQ +1 2 yes 0.5 0.8333 + +#include "ffnonbonded.itp" +#include "ffbonded.itp" +#include "gbsa.itp" + + diff --git a/pmx/data/mutff45/amber14sb_OL15.ff/gbsa.itp b/pmx/data/mutff45/amber14sb_OL15.ff/gbsa.itp new file mode 100644 index 00000000..632e8cbe --- /dev/null +++ b/pmx/data/mutff45/amber14sb_OL15.ff/gbsa.itp @@ -0,0 +1,38 @@ +[ implicit_genborn_params ] + +; atype sar st pi gbr hct +;Br 0.1 1 1 0.125 0.85 ; H +C 0.172 1 1.554 0.1875 0.72 ; C +CA 0.18 1 1.037 0.1875 0.72 ; C +CB 0.172 0.012 1.554 0.1875 0.72 ; C +CC 0.172 1 1.554 0.1875 0.72 ; C +CN 0.172 0.012 1.554 0.1875 0.72 ; C +CR 0.18 1 1.073 0.1875 0.72 ; C +CT 0.18 1 1.276 0.190 0.72 ; C +CV 0.18 1 1.073 0.1875 0.72 ; C +CW 0.18 1 1.073 0.1875 0.72 ; C +C* 0.172 0.012 1.554 0.1875 0.72 ; C +H 0.1 1 1 0.115 0.85 ; H +HC 0.1 1 1 0.125 0.85 ; H +H1 0.1 1 1 0.125 0.85 ; H +HA 0.1 1 1 0.125 0.85 ; H +H4 0.1 1 1 0.115 0.85 ; H +H5 0.1 1 1 0.125 0.85 ; H +HO 0.1 1 1 0.105 0.85 ; H +HS 0.1 1 1 0.125 0.85 ; H +HP 0.1 1 1 0.125 0.85 ; H +N 0.155 1 1.028 0.17063 0.79 ; N +NA 0.155 1 1.028 0.17063 0.79 ; N +NB 0.155 1 1.215 0.17063 0.79 ; N +N2 0.16 1 1.215 0.17063 0.79 ; N +N3 0.16 1 1.215 0.1625 0.79 ; N +O 0.15 1 0.926 0.148 0.85 ; O +OH 0.152 1 1.080 0.1535 0.85 ; O +O2 0.17 1 0.922 0.148 0.85 ; O +S 0.18 1 1.121 0.1775 0.96 ; S +SH 0.18 1 1.121 0.1775 0.96 ; S + +; masscenters for vsites do not have gbsa parameters + +MNH3 0 0 0 0 0 +MCH3 0 0 0 0 0 diff --git a/pmx/data/mutff45/amber14sb_OL15.ff/ions.itp b/pmx/data/mutff45/amber14sb_OL15.ff/ions.itp new file mode 100644 index 00000000..fe13ab66 --- /dev/null +++ b/pmx/data/mutff45/amber14sb_OL15.ff/ions.itp @@ -0,0 +1,88 @@ +[ moleculetype ] +; molname nrexcl +IB+ 1 ; big positive ion + +[ atoms ] +; id at type res nr residu name at name cg nr charge +1 IB 1 IB+ IB 1 1.00000 + + +[ moleculetype ] +; molname nrexcl +CA 1 + +[ atoms ] +; id at type res nr residu name at name cg nr charge +1 C0 1 CA CA 1 2.00000 + + +[ moleculetype ] +; molname nrexcl +CL 1 + +[ atoms ] +; id at type res nr residu name at name cg nr charge +1 Cl 1 CL CL 1 -1.00000 + + +[ moleculetype ] +; molname nrexcl +NA 1 + +[ atoms ] +; id at type res nr residu name at name cg nr charge +1 Na 1 NA NA 1 1.00000 + + +[ moleculetype ] +; molname nrexcl +MG 1 + +[ atoms ] +; id at type res nr residu name at name cg nr charge +1 MG 1 MG MG 1 2.00000 + + +[ moleculetype ] +; molname nrexcl +K 1 + +[ atoms ] +; id at type res nr residu name at name cg nr charge +1 K 1 K K 1 1.00000 + + +[ moleculetype ] +; molname nrexcl +RB 1 + +[ atoms ] +; id at type res nr residu name at name cg nr charge +1 Rb 1 RB RB 1 1.00000 + + +[ moleculetype ] +; molname nrexcl +CS 1 + +[ atoms ] +; id at type res nr residu name at name cg nr charge +1 Cs 1 CS CS 1 1.00000 + + +[ moleculetype ] +; molname nrexcl +LI 1 + +[ atoms ] +; id at type res nr residu name at name cg nr charge +1 Li 1 LI LI 1 1.00000 + + +[ moleculetype ] +; molname nrexcl +ZN 1 + +[ atoms ] +; id at type res nr residu name at name cg nr charge +1 Zn 1 ZN ZN 1 2.00000 diff --git a/pmx/data/mutff45/amber14sb_OL15.ff/rna.arn b/pmx/data/mutff45/amber14sb_OL15.ff/rna.arn new file mode 100644 index 00000000..ed58c405 --- /dev/null +++ b/pmx/data/mutff45/amber14sb_OL15.ff/rna.arn @@ -0,0 +1,8 @@ +RNA OP1 O1P +RNA OP2 O2P +RNA H2' H2'1 +RNA H2'' H2'2 +RNA H5' H5'1 +RNA H5'' H5'2 +RNA HO5' H5T +RNA HO3' H3T diff --git a/pmx/data/mutff45/amber14sb_OL15.ff/rna.hdb b/pmx/data/mutff45/amber14sb_OL15.ff/rna.hdb new file mode 100644 index 00000000..43aa32d9 --- /dev/null +++ b/pmx/data/mutff45/amber14sb_OL15.ff/rna.hdb @@ -0,0 +1,205 @@ +RA5 10 +1 2 H5T O5' C5' C4' +2 6 H5' C5' O5' C4' +1 5 H4' C4' C5' O4' C3' +1 5 H1' C1' O4' N9 C2' +1 1 H8 C8 N9 N7 +2 3 H6 N6 C6 C5 +1 1 H2 C2 N1 N3 +1 5 H3' C3' C4' C2' O3' +1 5 H2'1 C2' C1' C3' O2' +1 2 HO'2 O2' C2' C1' +RA 9 +2 6 H5' C5' O5' C4' +1 5 H4' C4' C5' O4' C3' +1 5 H1' C1' O4' N9 C2' +1 1 H8 C8 N9 N7 +2 3 H6 N6 C6 C5 +1 1 H2 C2 N1 N3 +1 5 H3' C3' C4' C2' O3' +1 5 H2'1 C2' C1' C3' O2' +1 2 HO'2 O2' C2' C1' +RA3 10 +2 6 H5' C5' O5' C4' +1 5 H4' C4' C5' O4' C3' +1 5 H1' C1' O4' N9 C2' +1 1 H8 C8 N9 N7 +2 3 H6 N6 C6 C5 +1 1 H2 C2 N1 N3 +1 5 H3' C3' C4' C2' O3' +1 5 H2'1 C2' C1' C3' O2' +1 2 HO'2 O2' C2' C1' +1 2 H3T O3' C3' C4' +RAN 11 +1 2 H5T O5' C5' C4' +2 6 H5' C5' O5' C4' +1 5 H4' C4' C5' O4' C3' +1 5 H1' C1' O4' N9 C2' +1 1 H8 C8 N9 N7 +2 3 H6 N6 C6 C5 +1 1 H2 C2 N1 N3 +1 5 H3' C3' C4' C2' O3' +1 5 H2'1 C2' C1' C3' O2' +1 2 HO'2 O2' C2' C1' +1 2 H3T O3' C3' C4' +DT5 9 +1 2 H5T O5' C5' C4' +2 6 H5' C5' O5' C4' +1 5 H4' C4' C5' O4' C3' +1 5 H1' C1' O4' N1 C2' +1 1 H6 C6 N1 C5 +3 4 H7 C7 C5 C6 +1 1 H3 N3 C4 C2 +1 5 H3' C3' C4' C2' O3' +2 6 H2' C2' C1' C3' +DT 8 +2 6 H5' C5' O5' C4' +1 5 H4' C4' C5' O4' C3' +1 5 H1' C1' O4' N1 C2' +1 1 H6 C6 N1 C5 +3 4 H7 C7 C5 C6 +1 1 H3 N3 C4 C2 +1 5 H3' C3' C4' C2' O3' +2 6 H2' C2' C1' C3' +DT3 9 +2 6 H5' C5' O5' C4' +1 5 H4' C4' C5' O4' C3' +1 5 H1' C1' O4' N1 C2' +1 1 H6 C6 N1 C5 +3 4 H7 C7 C5 C6 +1 1 H3 N3 C4 C2 +1 5 H3' C3' C4' C2' O3' +2 6 H2' C2' C1' C3' +1 2 H3T O3' C3' C4' +RU5 10 +1 2 H5T O5' C5' C4' +2 6 H5' C5' O5' C4' +1 5 H4' C4' C5' O4' C3' +1 5 H1' C1' O4' N1 C2' +1 1 H6 C6 N1 C5 +1 1 H5 C5 C6 C4 +1 1 H3 N3 C4 C2 +1 5 H3' C3' C4' C2' O3' +1 5 H2'1 C2' C1' C3' O2' +1 2 HO'2 O2' C2' C1' +RU 9 +2 6 H5' C5' O5' C4' +1 5 H4' C4' C5' O4' C3' +1 5 H1' C1' O4' N1 C2' +1 1 H6 C6 N1 C5 +1 1 H5 C5 C6 C4 +1 1 H3 N3 C4 C2 +1 5 H3' C3' C4' C2' O3' +1 5 H2'1 C2' C1' C3' O2' +1 2 HO'2 O2' C2' C1' +RU3 10 +2 6 H5' C5' O5' C4' +1 5 H4' C4' C5' O4' C3' +1 5 H1' C1' O4' N1 C2' +1 1 H6 C6 N1 C5 +1 1 H5 C5 C6 C4 +1 1 H3 N3 C4 C2 +1 5 H3' C3' C4' C2' O3' +1 5 H2'1 C2' C1' C3' O2' +1 2 HO'2 O2' C2' C1' +1 2 H3T O3' C3' C4' +RUN 11 +1 2 H5T O5' C5' C4' +2 6 H5' C5' O5' C4' +1 5 H4' C4' C5' O4' C3' +1 5 H1' C1' O4' N1 C2' +1 1 H6 C6 N1 C5 +1 1 H5 C5 C6 C4 +1 1 H3 N3 C4 C2 +1 5 H3' C3' C4' C2' O3' +1 5 H2'1 C2' C1' C3' O2' +1 2 HO'2 O2' C2' C1' +1 2 H3T O3' C3' C4' +RG5 10 +1 2 H5T O5' C5' C4' +2 6 H5' C5' O5' C4' +1 5 H4' C4' C5' O4' C3' +1 5 H1' C1' O4' N9 C2' +1 1 H8 C8 N9 N7 +1 1 H1 N1 C6 C2 +2 3 H2 N2 C2 N1 +1 5 H3' C3' C4' C2' O3' +1 5 H2'1 C2' C1' C3' O2' +1 2 HO'2 O2' C2' C1' +RG 9 +2 6 H5' C5' O5' C4' +1 5 H4' C4' C5' O4' C3' +1 5 H1' C1' O4' N9 C2' +1 1 H8 C8 N9 N7 +1 1 H1 N1 C6 C2 +2 3 H2 N2 C2 N1 +1 5 H3' C3' C4' C2' O3' +1 5 H2'1 C2' C1' C3' O2' +1 2 HO'2 O2' C2' C1' +RG3 10 +2 6 H5' C5' O5' C4' +1 5 H4' C4' C5' O4' C3' +1 5 H1' C1' O4' N9 C2' +1 1 H8 C8 N9 N7 +1 1 H1 N1 C6 C2 +2 3 H2 N2 C2 N1 +1 5 H3' C3' C4' C2' O3' +1 5 H2'1 C2' C1' C3' O2' +1 2 HO'2 O2' C2' C1' +1 2 H3T O3' C3' C4' +RGN 11 +1 2 H5T O5' C5' C4' +2 6 H5' C5' O5' C4' +1 5 H4' C4' C5' O4' C3' +1 5 H1' C1' O4' N9 C2' +1 1 H8 C8 N9 N7 +1 1 H1 N1 C6 C2 +2 3 H2 N2 C2 N1 +1 5 H3' C3' C4' C2' O3' +1 5 H2'1 C2' C1' C3' O2' +1 2 HO'2 O2' C2' C1' +1 2 H3T O3' C3' C4' +RC5 10 +1 2 H5T O5' C5' C4' +2 6 H5' C5' O5' C4' +1 5 H4' C4' C5' O4' C3' +1 5 H1' C1' O4' N1 C2' +1 1 H6 C6 N1 C5 +1 1 H5 C5 C6 C4 +2 3 H4 N4 C4 C5 +1 5 H3' C3' C4' C2' O3' +1 5 H2'1 C2' C1' C3' O2' +1 2 HO'2 O2' C2' C1' +RC 9 +2 6 H5' C5' O5' C4' +1 5 H4' C4' C5' O4' C3' +1 5 H1' C1' O4' N1 C2' +1 1 H6 C6 N1 C5 +1 1 H5 C5 C6 C4 +2 3 H4 N4 C4 C5 +1 5 H3' C3' C4' C2' O3' +1 5 H2'1 C2' C1' C3' O2' +1 2 HO'2 O2' C2' C1' +RC3 10 +2 6 H5' C5' O5' C4' +1 5 H4' C4' C5' O4' C3' +1 5 H1' C1' O4' N1 C2' +1 1 H6 C6 N1 C5 +1 1 H5 C5 C6 C4 +2 3 H4 N4 C4 C5 +1 5 H3' C3' C4' C2' O3' +1 5 H2'1 C2' C1' C3' O2' +1 2 HO'2 O2' C2' C1' +1 2 H3T O3' C3' C4' +RCN 11 +1 2 H5T O5' C5' C4' +2 6 H5' C5' O5' C4' +1 5 H4' C4' C5' O4' C3' +1 5 H1' C1' O4' N1 C2' +1 1 H6 C6 N1 C5 +1 1 H5 C5 C6 C4 +2 3 H4 N4 C4 C5 +1 5 H3' C3' C4' C2' O3' +1 5 H2'1 C2' C1' C3' O2' +1 2 HO'2 O2' C2' C1' +1 2 H3T O3' C3' C4' diff --git a/pmx/data/mutff45/amber14sb_OL15.ff/rna.r2b b/pmx/data/mutff45/amber14sb_OL15.ff/rna.r2b new file mode 100644 index 00000000..9893f433 --- /dev/null +++ b/pmx/data/mutff45/amber14sb_OL15.ff/rna.r2b @@ -0,0 +1,6 @@ +; rtp residue to rtp building block table +;GMX Force-field +A RA RA5 RA3 RAN +U RU RU5 RU3 RUN +C RC RC5 RC3 RCN +G RG RG5 RG3 RGN diff --git a/pmx/data/mutff45/amber14sb_OL15.ff/rna.rtp b/pmx/data/mutff45/amber14sb_OL15.ff/rna.rtp new file mode 100644 index 00000000..bf1a3dd1 --- /dev/null +++ b/pmx/data/mutff45/amber14sb_OL15.ff/rna.rtp @@ -0,0 +1,1232 @@ +[ bondedtypes ] +; Col 1: Type of bond +; Col 2: Type of angles +; Col 3: Type of proper dihedrals +; Col 4: Type of improper dihedrals +; Col 5: Generate all dihedrals if 1, only heavy atoms of 0. +; Col 6: Number of excluded neighbors for nonbonded interactions +; Col 7: Generate 1,4 interactions between pairs of hydrogens if 1 +; Col 8: Remove impropers over the same bond as a proper if it is 1 +; bonds angles dihedrals impropers all_dihedrals nrexcl HH14 RemoveDih + 1 1 9 4 1 3 1 0 + + +; 5' (XXF), 3' (XXT), non-terminal (XX), and monomer (XXN) nuc's + + +[ RA5 ] + [ atoms ] + H5T HO 0.42950 1 + O5' OH -0.62230 2 + C5' CI 0.05580 3 + H5'1 H1 0.06790 4 + H5'2 H1 0.06790 5 + C4' CT 0.10650 6 + H4' H1 0.11740 7 + O4' OS -0.35480 8 + C1' CT 0.03940 9 + H1' H2 0.20070 10 + N9 N* -0.02510 11 + C8 C5 0.20060 12 + H8 H5 0.15530 13 + N7 NB -0.60730 14 + C5 CB 0.05150 15 + C6 CA 0.70090 16 + N6 N2 -0.90190 17 + H61 H 0.41150 18 + H62 H 0.41150 19 + N1 NC -0.76150 20 + C2 CQ 0.58750 21 + H2 H5 0.04730 22 + N3 NC -0.69970 23 + C4 CB 0.30530 24 + C3' CT 0.20220 25 + H3' H1 0.06150 26 + C2' CT 0.06700 27 + H2'1 H1 0.09720 28 + O2' OH -0.61390 29 + HO'2 HO 0.41860 30 + O3' OS -0.52460 31 + [ bonds ] + H5T O5' + O5' C5' + C5' H5'1 + C5' H5'2 + C5' C4' + C4' H4' + C4' O4' + C4' C3' + O4' C1' + C1' H1' + C1' N9 + C1' C2' + N9 C8 + N9 C4 + C8 H8 + C8 N7 + N7 C5 + C5 C6 + C5 C4 + C6 N6 + C6 N1 + N6 H61 + N6 H62 + N1 C2 + C2 H2 + C2 N3 + N3 C4 + C3' H3' + C3' C2' + C3' O3' + C2' H2'1 + C2' O2' + O2' HO'2 + [ impropers ] + C4 C8 N9 C1' + C6 H61 N6 H62 + N9 N7 C8 H8 + N1 N3 C2 H2 + C5 N6 C6 N1 + +[ RA ] + [ atoms ] + P P 1.16620 1 + O1P O2 -0.77600 2 + O2P O2 -0.77600 3 + O5' OS -0.49890 4 + C5' CI 0.05580 5 + H5'1 H1 0.06790 6 + H5'2 H1 0.06790 7 + C4' CT 0.10650 8 + H4' H1 0.11740 9 + O4' OS -0.35480 10 + C1' CT 0.03940 11 + H1' H2 0.20070 12 + N9 N* -0.02510 13 + C8 C5 0.20060 14 + H8 H5 0.15530 15 + N7 NB -0.60730 16 + C5 CB 0.05150 17 + C6 CA 0.70090 18 + N6 N2 -0.90190 19 + H61 H 0.41150 20 + H62 H 0.41150 21 + N1 NC -0.76150 22 + C2 CQ 0.58750 23 + H2 H5 0.04730 24 + N3 NC -0.69970 25 + C4 CB 0.30530 26 + C3' CT 0.20220 27 + H3' H1 0.06150 28 + C2' CT 0.06700 29 + H2'1 H1 0.09720 30 + O2' OH -0.61390 31 + HO'2 HO 0.41860 32 + O3' OS -0.52460 33 + [ bonds ] + P O1P + P O2P + P O5' + O5' C5' + C5' H5'1 + C5' H5'2 + C5' C4' + C4' H4' + C4' O4' + C4' C3' + O4' C1' + C1' H1' + C1' N9 + C1' C2' + N9 C8 + N9 C4 + C8 H8 + C8 N7 + N7 C5 + C5 C6 + C5 C4 + C6 N6 + C6 N1 + N6 H61 + N6 H62 + N1 C2 + C2 H2 + C2 N3 + N3 C4 + C3' H3' + C3' C2' + C3' O3' + C2' H2'1 + C2' O2' + O2' HO'2 + -O3' P + [ impropers ] + C4 C8 N9 C1' + C6 H61 N6 H62 + N9 N7 C8 H8 + N1 N3 C2 H2 + C5 N6 C6 N1 + +[ RA3 ] + [ atoms ] + P P 1.16620 1 + O1P O2 -0.77600 2 + O2P O2 -0.77600 3 + O5' OS -0.49890 4 + C5' CI 0.05580 5 + H5'1 H1 0.06790 6 + H5'2 H1 0.06790 7 + C4' CT 0.10650 8 + H4' H1 0.11740 9 + O4' OS -0.35480 10 + C1' CT 0.03940 11 + H1' H2 0.20070 12 + N9 N* -0.02510 13 + C8 C5 0.20060 14 + H8 H5 0.15530 15 + N7 NB -0.60730 16 + C5 CB 0.05150 17 + C6 CA 0.70090 18 + N6 N2 -0.90190 19 + H61 H 0.41150 20 + H62 H 0.41150 21 + N1 NC -0.76150 22 + C2 CQ 0.58750 23 + H2 H5 0.04730 24 + N3 NC -0.69970 25 + C4 CB 0.30530 26 + C3' CT 0.20220 27 + H3' H1 0.06150 28 + C2' CT 0.06700 29 + H2'1 H1 0.09720 30 + O2' OH -0.61390 31 + HO'2 HO 0.41860 32 + O3' OH -0.65410 33 + H3T HO 0.43760 34 + [ bonds ] + P O1P + P O2P + P O5' + O5' C5' + C5' H5'1 + C5' H5'2 + C5' C4' + C4' H4' + C4' O4' + C4' C3' + O4' C1' + C1' H1' + C1' N9 + C1' C2' + N9 C8 + N9 C4 + C8 H8 + C8 N7 + N7 C5 + C5 C6 + C5 C4 + C6 N6 + C6 N1 + N6 H61 + N6 H62 + N1 C2 + C2 H2 + C2 N3 + N3 C4 + C3' H3' + C3' C2' + C3' O3' + C2' H2'1 + C2' O2' + O2' HO'2 + O3' H3T + -O3' P + [ impropers ] + C4 C8 N9 C1' + C6 H61 N6 H62 + N9 N7 C8 H8 + N1 N3 C2 H2 + C5 N6 C6 N1 + +[ RAN ] + [ atoms ] + H5T HO 0.42950 1 + O5' OH -0.62230 2 + C5' CI 0.05580 3 + H5'1 H1 0.06790 4 + H5'2 H1 0.06790 5 + C4' CT 0.10650 6 + H4' H1 0.11740 7 + O4' OS -0.35480 8 + C1' CT 0.03940 9 + H1' H2 0.20070 10 + N9 N* -0.02510 11 + C8 C5 0.20060 12 + H8 H5 0.15530 13 + N7 NB -0.60730 14 + C5 CB 0.05150 15 + C6 CA 0.70090 16 + N6 N2 -0.90190 17 + H61 H 0.41150 18 + H62 H 0.41150 19 + N1 NC -0.76150 20 + C2 CQ 0.58750 21 + H2 H5 0.04730 22 + N3 NC -0.69970 23 + C4 CB 0.30530 24 + C3' CT 0.20220 25 + H3' H1 0.06150 26 + C2' CT 0.06700 27 + H2'1 H1 0.09720 28 + O2' OH -0.61390 29 + HO'2 HO 0.41860 30 + O3' OH -0.65410 31 + H3T HO 0.43760 32 + [ bonds ] + H5T O5' + O5' C5' + C5' H5'1 + C5' H5'2 + C5' C4' + C4' H4' + C4' O4' + C4' C3' + O4' C1' + C1' H1' + C1' N9 + C1' C2' + N9 C8 + N9 C4 + C8 H8 + C8 N7 + N7 C5 + C5 C6 + C5 C4 + C6 N6 + C6 N1 + N6 H61 + N6 H62 + N1 C2 + C2 H2 + C2 N3 + N3 C4 + C3' H3' + C3' C2' + C3' O3' + C2' H2'1 + C2' O2' + O2' HO'2 + O3' H3T + [ impropers ] + C4 C8 N9 C1' + C6 H61 N6 H62 + N9 N7 C8 H8 + N1 N3 C2 H2 + C5 N6 C6 N1 + + +[ RU5 ] + [ atoms ] + H5T HO 0.42950 1 + O5' OH -0.62230 2 + C5' CI 0.05580 3 + H5'1 H1 0.06790 4 + H5'2 H1 0.06790 5 + C4' CT 0.10650 6 + H4' H1 0.11740 7 + O4' OS -0.35480 8 + C1' CT 0.06740 9 + H1' H2 0.18240 10 + N1 N* 0.04180 11 + C6 CS -0.11260 12 + H6 H4 0.21880 13 + C5 CS -0.36350 14 + H5 HA 0.18110 15 + C4 C 0.59520 16 + O4 O -0.57610 17 + N3 NA -0.35490 18 + H3 H 0.31540 19 + C2 C 0.46870 20 + O2 O -0.54770 21 + C3' CT 0.20220 22 + H3' H1 0.06150 23 + C2' CT 0.06700 24 + H2'1 H1 0.09720 25 + O2' OH -0.61390 26 + HO'2 HO 0.41860 27 + O3' OS -0.52460 28 + [ bonds ] + H5T O5' + O5' C5' + C5' H5'1 + C5' H5'2 + C5' C4' + C4' H4' + C4' O4' + C4' C3' + O4' C1' + C1' H1' + C1' N1 + C1' C2' + N1 C6 + N1 C2 + C6 H6 + C6 C5 + C5 H5 + C5 C4 + C4 O4 + C4 N3 + N3 H3 + N3 C2 + C2 O2 + C3' H3' + C3' C2' + C3' O3' + C2' H2'1 + C2' O2' + O2' HO'2 + [ impropers ] + C2 C6 N1 C1' + C6 C4 C5 H5 + N1 N3 C2 O2 + C5 N3 C4 O4 + C4 C2 N3 H3 + N1 C5 C6 H6 + +[ RU ] + [ atoms ] + P P 1.16620 1 + O1P O2 -0.77600 2 + O2P O2 -0.77600 3 + O5' OS -0.49890 4 + C5' CI 0.05580 5 + H5'1 H1 0.06790 6 + H5'2 H1 0.06790 7 + C4' CT 0.10650 8 + H4' H1 0.11740 9 + O4' OS -0.35480 10 + C1' CT 0.06740 11 + H1' H2 0.18240 12 + N1 N* 0.04180 13 + C6 CS -0.11260 14 + H6 H4 0.21880 15 + C5 CS -0.36350 16 + H5 HA 0.18110 17 + C4 C 0.59520 18 + O4 O -0.57610 19 + N3 NA -0.35490 20 + H3 H 0.31540 21 + C2 C 0.46870 22 + O2 O -0.54770 23 + C3' CT 0.20220 24 + H3' H1 0.06150 25 + C2' CT 0.06700 26 + H2'1 H1 0.09720 27 + O2' OH -0.61390 28 + HO'2 HO 0.41860 29 + O3' OS -0.52460 30 + [ bonds ] + P O1P + P O2P + P O5' + O5' C5' + C5' H5'1 + C5' H5'2 + C5' C4' + C4' H4' + C4' O4' + C4' C3' + O4' C1' + C1' H1' + C1' N1 + C1' C2' + N1 C6 + N1 C2 + C6 H6 + C6 C5 + C5 H5 + C5 C4 + C4 O4 + C4 N3 + N3 H3 + N3 C2 + C2 O2 + C3' H3' + C3' C2' + C3' O3' + C2' H2'1 + C2' O2' + O2' HO'2 + -O3' P + [ impropers ] + C2 C6 N1 C1' + C6 C4 C5 H5 + N1 N3 C2 O2 + C5 N3 C4 O4 + C4 C2 N3 H3 + N1 C5 C6 H6 + +[ RU3 ] + [ atoms ] + P P 1.16620 1 + O1P O2 -0.77600 2 + O2P O2 -0.77600 3 + O5' OS -0.49890 4 + C5' CI 0.05580 5 + H5'1 H1 0.06790 6 + H5'2 H1 0.06790 7 + C4' CT 0.10650 8 + H4' H1 0.11740 9 + O4' OS -0.35480 10 + C1' CT 0.06740 11 + H1' H2 0.18240 12 + N1 N* 0.04180 13 + C6 CS -0.11260 14 + H6 H4 0.21880 15 + C5 CS -0.36350 16 + H5 HA 0.18110 17 + C4 C 0.59520 18 + O4 O -0.57610 19 + N3 NA -0.35490 20 + H3 H 0.31540 21 + C2 C 0.46870 22 + O2 O -0.54770 23 + C3' CT 0.20220 24 + H3' H1 0.06150 25 + C2' CT 0.06700 26 + H2'1 H1 0.09720 27 + O2' OH -0.61390 28 + HO'2 HO 0.41860 29 + O3' OH -0.65410 30 + H3T HO 0.43760 31 + [ bonds ] + P O1P + P O2P + P O5' + O5' C5' + C5' H5'1 + C5' H5'2 + C5' C4' + C4' H4' + C4' O4' + C4' C3' + O4' C1' + C1' H1' + C1' N1 + C1' C2' + N1 C6 + N1 C2 + C6 H6 + C6 C5 + C5 H5 + C5 C4 + C4 O4 + C4 N3 + N3 H3 + N3 C2 + C2 O2 + C3' H3' + C3' C2' + C3' O3' + C2' H2'1 + C2' O2' + O2' HO'2 + O3' H3T + -O3' P + [ impropers ] + C2 C6 N1 C1' + C6 C4 C5 H5 + N1 N3 C2 O2 + C5 N3 C4 O4 + C4 C2 N3 H3 + N1 C5 C6 H6 + +[ RUN ] + [ atoms ] + H5T HO 0.42950 1 + O5' OH -0.62230 2 + C5' CI 0.05580 3 + H5'1 H1 0.06790 4 + H5'2 H1 0.06790 5 + C4' CT 0.10650 6 + H4' H1 0.11740 7 + O4' OS -0.35480 8 + C1' CT 0.06740 9 + H1' H2 0.18240 10 + N1 N* 0.04180 11 + C6 CS -0.11260 12 + H6 H4 0.21880 13 + C5 CS -0.36350 14 + H5 HA 0.18110 15 + C4 C 0.59520 16 + O4 O -0.57610 17 + N3 NA -0.35490 18 + H3 H 0.31540 19 + C2 C 0.46870 20 + O2 O -0.54770 21 + C3' CT 0.20220 22 + H3' H1 0.06150 23 + C2' CT 0.06700 24 + H2'1 H1 0.09720 25 + O2' OH -0.61390 26 + HO'2 HO 0.41860 27 + O3' OH -0.65410 28 + H3T HO 0.43760 29 + [ bonds ] + H5T O5' + O5' C5' + C5' H5'1 + C5' H5'2 + C5' C4' + C4' H4' + C4' O4' + C4' C3' + O4' C1' + C1' H1' + C1' N1 + C1' C2' + N1 C6 + N1 C2 + C6 H6 + C6 C5 + C5 H5 + C5 C4 + C4 O4 + C4 N3 + N3 H3 + N3 C2 + C2 O2 + C3' H3' + C3' C2' + C3' O3' + C2' H2'1 + C2' O2' + O2' HO'2 + O3' H3T + [ impropers ] + C2 C6 N1 C1' + C6 C4 C5 H5 + N1 N3 C2 O2 + C5 N3 C4 O4 + C4 C2 N3 H3 + N1 C5 C6 H6 + + +[ RG5 ] + [ atoms ] + H5T HO 0.42950 1 + O5' OH -0.62230 2 + C5' CI 0.05580 3 + H5'1 H1 0.06790 4 + H5'2 H1 0.06790 5 + C4' CT 0.10650 6 + H4' H1 0.11740 7 + O4' OS -0.35480 8 + C1' CT 0.01910 9 + H1' H2 0.20060 10 + N9 N* 0.04920 11 + C8 CP 0.13740 12 + H8 H5 0.16400 13 + N7 NB -0.57090 14 + C5 CB 0.17440 15 + C6 C 0.47700 16 + O6 O -0.55970 17 + N1 NA -0.47870 18 + H1 H 0.34240 19 + C2 CA 0.76570 20 + N2 N2 -0.96720 21 + H21 H 0.43640 22 + H22 H 0.43640 23 + N3 NC -0.63230 24 + C4 CB 0.12220 25 + C3' CT 0.20220 26 + H3' H1 0.06150 27 + C2' CT 0.06700 28 + H2'1 H1 0.09720 29 + O2' OH -0.61390 30 + HO'2 HO 0.41860 31 + O3' OS -0.52460 32 + [ bonds ] + H5T O5' + O5' C5' + C5' H5'1 + C5' H5'2 + C5' C4' + C4' H4' + C4' O4' + C4' C3' + O4' C1' + C1' H1' + C1' N9 + C1' C2' + N9 C8 + N9 C4 + C8 H8 + C8 N7 + N7 C5 + C5 C6 + C5 C4 + C6 O6 + C6 N1 + N1 H1 + N1 C2 + C2 N2 + C2 N3 + N2 H21 + N2 H22 + N3 C4 + C3' H3' + C3' C2' + C3' O3' + C2' H2'1 + C2' O2' + O2' HO'2 + [ impropers ] + C4 C8 N9 C1' + C5 N1 C6 O6 + C6 C2 N1 H1 + C2 H21 N2 H22 + N9 N7 C8 H8 + N2 N1 C2 N3 + +[ RG ] + [ atoms ] + P P 1.16620 1 + O1P O2 -0.77600 2 + O2P O2 -0.77600 3 + O5' OS -0.49890 4 + C5' CI 0.05580 5 + H5'1 H1 0.06790 6 + H5'2 H1 0.06790 7 + C4' CT 0.10650 8 + H4' H1 0.11740 9 + O4' OS -0.35480 10 + C1' CT 0.01910 11 + H1' H2 0.20060 12 + N9 N* 0.04920 13 + C8 CP 0.13740 14 + H8 H5 0.16400 15 + N7 NB -0.57090 16 + C5 CB 0.17440 17 + C6 C 0.47700 18 + O6 O -0.55970 19 + N1 NA -0.47870 20 + H1 H 0.34240 21 + C2 CA 0.76570 22 + N2 N2 -0.96720 23 + H21 H 0.43640 24 + H22 H 0.43640 25 + N3 NC -0.63230 26 + C4 CB 0.12220 27 + C3' CT 0.20220 28 + H3' H1 0.06150 29 + C2' CT 0.06700 30 + H2'1 H1 0.09720 31 + O2' OH -0.61390 32 + HO'2 HO 0.41860 33 + O3' OS -0.52460 34 + [ bonds ] + P O1P + P O2P + P O5' + O5' C5' + C5' H5'1 + C5' H5'2 + C5' C4' + C4' H4' + C4' O4' + C4' C3' + O4' C1' + C1' H1' + C1' N9 + C1' C2' + N9 C8 + N9 C4 + C8 H8 + C8 N7 + N7 C5 + C5 C6 + C5 C4 + C6 O6 + C6 N1 + N1 H1 + N1 C2 + C2 N2 + C2 N3 + N2 H21 + N2 H22 + N3 C4 + C3' H3' + C3' C2' + C3' O3' + C2' H2'1 + C2' O2' + O2' HO'2 + -O3' P + [ impropers ] + C4 C8 N9 C1' + C5 N1 C6 O6 + C6 C2 N1 H1 + C2 H21 N2 H22 + N9 N7 C8 H8 + N2 N1 C2 N3 + +[ RG3 ] + [ atoms ] + P P 1.16620 1 + O1P O2 -0.77600 2 + O2P O2 -0.77600 3 + O5' OS -0.49890 4 + C5' CI 0.05580 5 + H5'1 H1 0.06790 6 + H5'2 H1 0.06790 7 + C4' CT 0.10650 8 + H4' H1 0.11740 9 + O4' OS -0.35480 10 + C1' CT 0.01910 11 + H1' H2 0.20060 12 + N9 N* 0.04920 13 + C8 CP 0.13740 14 + H8 H5 0.16400 15 + N7 NB -0.57090 16 + C5 CB 0.17440 17 + C6 C 0.47700 18 + O6 O -0.55970 19 + N1 NA -0.47870 20 + H1 H 0.34240 21 + C2 CA 0.76570 22 + N2 N2 -0.96720 23 + H21 H 0.43640 24 + H22 H 0.43640 25 + N3 NC -0.63230 26 + C4 CB 0.12220 27 + C3' CT 0.20220 28 + H3' H1 0.06150 29 + C2' CT 0.06700 30 + H2'1 H1 0.09720 31 + O2' OH -0.61390 32 + HO'2 HO 0.41860 33 + O3' OH -0.65410 34 + H3T HO 0.43760 35 + [ bonds ] + P O1P + P O2P + P O5' + O5' C5' + C5' H5'1 + C5' H5'2 + C5' C4' + C4' H4' + C4' O4' + C4' C3' + O4' C1' + C1' H1' + C1' N9 + C1' C2' + N9 C8 + N9 C4 + C8 H8 + C8 N7 + N7 C5 + C5 C6 + C5 C4 + C6 O6 + C6 N1 + N1 H1 + N1 C2 + C2 N2 + C2 N3 + N2 H21 + N2 H22 + N3 C4 + C3' H3' + C3' C2' + C3' O3' + C2' H2'1 + C2' O2' + O2' HO'2 + O3' H3T + -O3' P + [ impropers ] + C4 C8 N9 C1' + C5 N1 C6 O6 + C6 C2 N1 H1 + C2 H21 N2 H22 + N9 N7 C8 H8 + N2 N1 C2 N3 + +[ RGN ] + [ atoms ] + H5T HO 0.42950 1 + O5' OH -0.62230 2 + C5' CI 0.05580 3 + H5'1 H1 0.06790 4 + H5'2 H1 0.06790 5 + C4' CT 0.10650 6 + H4' H1 0.11740 7 + O4' OS -0.35480 8 + C1' CT 0.01910 9 + H1' H2 0.20060 10 + N9 N* 0.04920 11 + C8 CP 0.13740 12 + H8 H5 0.16400 13 + N7 NB -0.57090 14 + C5 CB 0.17440 15 + C6 C 0.47700 16 + O6 O -0.55970 17 + N1 NA -0.47870 18 + H1 H 0.34240 19 + C2 CA 0.76570 20 + N2 N2 -0.96720 21 + H21 H 0.43640 22 + H22 H 0.43640 23 + N3 NC -0.63230 24 + C4 CB 0.12220 25 + C3' CT 0.20220 26 + H3' H1 0.06150 27 + C2' CT 0.06700 28 + H2'1 H1 0.09720 29 + O2' OH -0.61390 30 + HO'2 HO 0.41860 31 + O3' OH -0.65410 32 + H3T HO 0.43760 33 + [ bonds ] + H5T O5' + O5' C5' + C5' H5'1 + C5' H5'2 + C5' C4' + C4' H4' + C4' O4' + C4' C3' + O4' C1' + C1' H1' + C1' N9 + C1' C2' + N9 C8 + N9 C4 + C8 H8 + C8 N7 + N7 C5 + C5 C6 + C5 C4 + C6 O6 + C6 N1 + N1 H1 + N1 C2 + C2 N2 + C2 N3 + N2 H21 + N2 H22 + N3 C4 + C3' H3' + C3' C2' + C3' O3' + C2' H2'1 + C2' O2' + O2' HO'2 + O3' H3T + [ impropers ] + C4 C8 N9 C1' + C5 N1 C6 O6 + C6 C2 N1 H1 + C2 H21 N2 H22 + N9 N7 C8 H8 + N2 N1 C2 N3 + + +[ RC5 ] + [ atoms ] + H5T HO 0.42950 1 + O5' OH -0.62230 2 + C5' CI 0.05580 3 + H5'1 H1 0.06790 4 + H5'2 H1 0.06790 5 + C4' CT 0.10650 6 + H4' H1 0.11740 7 + O4' OS -0.35480 8 + C1' CT 0.00660 9 + H1' H2 0.20290 10 + N1 N* -0.04840 11 + C6 C4 0.00530 12 + H6 H4 0.19580 13 + C5 C4 -0.52150 14 + H5 HA 0.19280 15 + C4 CA 0.81850 16 + N4 N2 -0.95300 17 + H41 H 0.42340 18 + H42 H 0.42340 19 + N3 NC -0.75840 20 + C2 C 0.75380 21 + O2 O -0.62520 22 + C3' CT 0.20220 23 + H3' H1 0.06150 24 + C2' CT 0.06700 25 + H2'1 H1 0.09720 26 + O2' OH -0.61390 27 + HO'2 HO 0.41860 28 + O3' OS -0.52460 29 + [ bonds ] + H5T O5' + O5' C5' + C5' H5'1 + C5' H5'2 + C5' C4' + C4' H4' + C4' O4' + C4' C3' + O4' C1' + C1' H1' + C1' N1 + C1' C2' + N1 C6 + N1 C2 + C6 H6 + C6 C5 + C5 H5 + C5 C4 + C4 N4 + C4 N3 + N4 H41 + N4 H42 + N3 C2 + C2 O2 + C3' H3' + C3' C2' + C3' O3' + C2' H2'1 + C2' O2' + O2' HO'2 + [ impropers ] + C2 C6 N1 C1' + N1 N3 C2 O2 + C4 H41 N4 H42 + N1 C5 C6 H6 + C6 C4 C5 H5 + C5 N4 C4 N3 + +[ RC ] + [ atoms ] + P P 1.16620 1 + O1P O2 -0.77600 2 + O2P O2 -0.77600 3 + O5' OS -0.49890 4 + C5' CI 0.05580 5 + H5'1 H1 0.06790 6 + H5'2 H1 0.06790 7 + C4' CT 0.10650 8 + H4' H1 0.11740 9 + O4' OS -0.35480 10 + C1' CT 0.00660 11 + H1' H2 0.20290 12 + N1 N* -0.04840 13 + C6 C4 0.00530 14 + H6 H4 0.19580 15 + C5 C4 -0.52150 16 + H5 HA 0.19280 17 + C4 CA 0.81850 18 + N4 N2 -0.95300 19 + H41 H 0.42340 20 + H42 H 0.42340 21 + N3 NC -0.75840 22 + C2 C 0.75380 23 + O2 O -0.62520 24 + C3' CT 0.20220 25 + H3' H1 0.06150 26 + C2' CT 0.06700 27 + H2'1 H1 0.09720 28 + O2' OH -0.61390 29 + HO'2 HO 0.41860 30 + O3' OS -0.52460 31 + [ bonds ] + P O1P + P O2P + P O5' + O5' C5' + C5' H5'1 + C5' H5'2 + C5' C4' + C4' H4' + C4' O4' + C4' C3' + O4' C1' + C1' H1' + C1' N1 + C1' C2' + N1 C6 + N1 C2 + C6 H6 + C6 C5 + C5 H5 + C5 C4 + C4 N4 + C4 N3 + N4 H41 + N4 H42 + N3 C2 + C2 O2 + C3' H3' + C3' C2' + C3' O3' + C2' H2'1 + C2' O2' + O2' HO'2 + -O3' P + [ impropers ] + C2 C6 N1 C1' + N1 N3 C2 O2 + C4 H41 N4 H42 + N1 C5 C6 H6 + C6 C4 C5 H5 + C5 N4 C4 N3 + +[ RC3 ] + [ atoms ] + P P 1.16620 1 + O1P O2 -0.77600 2 + O2P O2 -0.77600 3 + O5' OS -0.49890 4 + C5' CI 0.05580 5 + H5'1 H1 0.06790 6 + H5'2 H1 0.06790 7 + C4' CT 0.10650 8 + H4' H1 0.11740 9 + O4' OS -0.35480 10 + C1' CT 0.00660 11 + H1' H2 0.20290 12 + N1 N* -0.04840 13 + C6 C4 0.00530 14 + H6 H4 0.19580 15 + C5 C4 -0.52150 16 + H5 HA 0.19280 17 + C4 CA 0.81850 18 + N4 N2 -0.95300 19 + H41 H 0.42340 20 + H42 H 0.42340 21 + N3 NC -0.75840 22 + C2 C 0.75380 23 + O2 O -0.62520 24 + C3' CT 0.20220 25 + H3' H1 0.06150 26 + C2' CT 0.06700 27 + H2'1 H1 0.09720 28 + O2' OH -0.61390 29 + HO'2 HO 0.41860 30 + O3' OH -0.65410 31 + H3T HO 0.43760 32 + [ bonds ] + P O1P + P O2P + P O5' + O5' C5' + C5' H5'1 + C5' H5'2 + C5' C4' + C4' H4' + C4' O4' + C4' C3' + O4' C1' + C1' H1' + C1' N1 + C1' C2' + N1 C6 + N1 C2 + C6 H6 + C6 C5 + C5 H5 + C5 C4 + C4 N4 + C4 N3 + N4 H41 + N4 H42 + N3 C2 + C2 O2 + C3' H3' + C3' C2' + C3' O3' + C2' H2'1 + C2' O2' + O2' HO'2 + O3' H3T + -O3' P + [ impropers ] + C2 C6 N1 C1' + N1 N3 C2 O2 + C4 H41 N4 H42 + N1 C5 C6 H6 + C6 C4 C5 H5 + C5 N4 C4 N3 + +[ RCN ] + [ atoms ] + H5T HO 0.42950 1 + O5' OH -0.62230 2 + C5' CI 0.05580 3 + H5'1 H1 0.06790 4 + H5'2 H1 0.06790 5 + C4' CT 0.10650 6 + H4' H1 0.11740 7 + O4' OS -0.35480 8 + C1' CT 0.00660 9 + H1' H2 0.20290 10 + N1 N* -0.04840 11 + C6 C4 0.00530 12 + H6 H4 0.19580 13 + C5 C4 -0.52150 14 + H5 HA 0.19280 15 + C4 CA 0.81850 16 + N4 N2 -0.95300 17 + H41 H 0.42340 18 + H42 H 0.42340 19 + N3 NC -0.75840 20 + C2 C 0.75380 21 + O2 O -0.62520 22 + C3' CT 0.20220 23 + H3' H1 0.06150 24 + C2' CT 0.06700 25 + H2'1 H1 0.09720 26 + O2' OH -0.61390 27 + HO'2 HO 0.41860 28 + O3' OH -0.65410 29 + H3T HO 0.43760 30 + [ bonds ] + H5T O5' + O5' C5' + C5' H5'1 + C5' H5'2 + C5' C4' + C4' H4' + C4' O4' + C4' C3' + O4' C1' + C1' H1' + C1' N1 + C1' C2' + N1 C6 + N1 C2 + C6 H6 + C6 C5 + C5 H5 + C5 C4 + C4 N4 + C4 N3 + N4 H41 + N4 H42 + N3 C2 + C2 O2 + C3' H3' + C3' C2' + C3' O3' + C2' H2'1 + C2' O2' + O2' HO'2 + O3' H3T + [ impropers ] + C2 C6 N1 C1' + N1 N3 C2 O2 + C4 H41 N4 H42 + N1 C5 C6 H6 + C6 C4 C5 H5 + C5 N4 C4 N3 + + diff --git a/pmx/data/mutff45/amber14sb_OL15.ff/spc.itp b/pmx/data/mutff45/amber14sb_OL15.ff/spc.itp new file mode 100644 index 00000000..1e7cae53 --- /dev/null +++ b/pmx/data/mutff45/amber14sb_OL15.ff/spc.itp @@ -0,0 +1,33 @@ +[ moleculetype ] +; molname nrexcl +SOL 2 + +[ atoms ] +; id at type res nr res name at name cg nr charge mass + 1 OW_spc 1 SOL OW 1 -0.82 15.99940 + 2 HW_spc 1 SOL HW1 1 0.41 1.00800 + 3 HW_spc 1 SOL HW2 1 0.41 1.00800 + +#ifndef FLEXIBLE + +[ settles ] +; OW funct doh dhh +1 1 0.1 0.16330 + +[ exclusions ] +1 2 3 +2 1 3 +3 1 2 + +#else + +[ bonds ] +; i j funct length force.c. +1 2 1 0.1 345000 0.1 345000 +1 3 1 0.1 345000 0.1 345000 + +[ angles ] +; i j k funct angle force.c. +2 1 3 1 109.47 383 109.47 383 + +#endif diff --git a/pmx/data/mutff45/amber14sb_OL15.ff/spce.itp b/pmx/data/mutff45/amber14sb_OL15.ff/spce.itp new file mode 100644 index 00000000..7c97bfea --- /dev/null +++ b/pmx/data/mutff45/amber14sb_OL15.ff/spce.itp @@ -0,0 +1,33 @@ +[ moleculetype ] +; molname nrexcl +SOL 2 + +[ atoms ] +; id at type res nr res name at name cg nr charge mass + 1 OW_spc 1 SOL OW 1 -0.8476 15.99940 + 2 HW_spc 1 SOL HW1 1 0.4238 1.00800 + 3 HW_spc 1 SOL HW2 1 0.4238 1.00800 + +#ifndef FLEXIBLE + +[ settles ] +; OW funct doh dhh +1 1 0.1 0.16330 + +[ exclusions ] +1 2 3 +2 1 3 +3 1 2 + +#else + +[ bonds ] +; i j funct length force.c. +1 2 1 0.1 345000 0.1 345000 +1 3 1 0.1 345000 0.1 345000 + +[ angles ] +; i j k funct angle force.c. +2 1 3 1 109.47 383 109.47 383 + +#endif diff --git a/pmx/data/mutff45/amber14sb_OL15.ff/tip3p.itp b/pmx/data/mutff45/amber14sb_OL15.ff/tip3p.itp new file mode 100644 index 00000000..ced4ece6 --- /dev/null +++ b/pmx/data/mutff45/amber14sb_OL15.ff/tip3p.itp @@ -0,0 +1,34 @@ +[ moleculetype ] +; molname nrexcl +SOL 2 + +[ atoms ] +; id at type res nr res name at name cg nr charge mass + 1 OW 1 SOL OW 1 -0.834 16.00000 + 2 HW 1 SOL HW1 1 0.417 1.00800 + 3 HW 1 SOL HW2 1 0.417 1.00800 + +#ifndef FLEXIBLE + +[ settles ] +; OW funct doh dhh +1 1 0.09572 0.15139 + +[ exclusions ] +1 2 3 +2 1 3 +3 1 2 + +#else + +[ bonds ] +; i j funct length force_constant +1 2 1 0.09572 502416.0 0.09572 502416.0 +1 3 1 0.09572 502416.0 0.09572 502416.0 + + +[ angles ] +; i j k funct angle force_constant +2 1 3 1 104.52 628.02 104.52 628.02 + +#endif diff --git a/pmx/data/mutff45/amber14sb_OL15.ff/tip4p.itp b/pmx/data/mutff45/amber14sb_OL15.ff/tip4p.itp new file mode 100644 index 00000000..3f926ca8 --- /dev/null +++ b/pmx/data/mutff45/amber14sb_OL15.ff/tip4p.itp @@ -0,0 +1,55 @@ +[ moleculetype ] +; molname nrexcl +SOL 2 + +[ atoms ] +; id at type res nr res name at name cg nr charge mass + 1 OW_tip4p 1 SOL OW 1 0 16.00000 + 2 HW_tip4p 1 SOL HW1 1 0.52 1.00800 + 3 HW_tip4p 1 SOL HW2 1 0.52 1.00800 + 4 MW 1 SOL MW 1 -1.04 0.00000 + +#ifndef FLEXIBLE + +[ settles ] +; i funct doh dhh +1 1 0.09572 0.15139 + +#else + +[ bonds ] +; i j funct length force.c. +1 2 1 0.09572 502416.0 0.09572 502416.0 +1 3 1 0.09572 502416.0 0.09572 502416.0 + +[ angles ] +; i j k funct angle force.c. +2 1 3 1 104.52 628.02 104.52 628.02 + +#endif + + +[ virtual_sites3 ] +; Vsite from funct a b +4 1 2 3 1 0.128012065 0.128012065 + + +[ exclusions ] +1 2 3 4 +2 1 3 4 +3 1 2 4 +4 1 2 3 + + +; The position of the virtual site is computed as follows: +; +; O +; +; V +; +; H H +; +; const = distance (OV) / [ cos (angle(VOH)) * distance (OH) ] +; 0.015 nm / [ cos (52.26 deg) * 0.09572 nm ] +; +; Vsite pos x4 = x1 + a*(x2-x1) + b*(x3-x1) diff --git a/pmx/data/mutff45/amber14sb_OL15.ff/tip4pew.itp b/pmx/data/mutff45/amber14sb_OL15.ff/tip4pew.itp new file mode 100644 index 00000000..571bec64 --- /dev/null +++ b/pmx/data/mutff45/amber14sb_OL15.ff/tip4pew.itp @@ -0,0 +1,56 @@ +[ moleculetype ] +; molname nrexcl +SOL 2 + +[ atoms ] +; id at type res nr res name at name cg nr charge mass + 1 OW_tip4pew 1 SOL OW 1 0 16.00000 + 2 HW_tip4pew 1 SOL HW1 1 0.52422 1.00800 + 3 HW_tip4pew 1 SOL HW2 1 0.52422 1.00800 + 4 MW 1 SOL MW 1 -1.04844 0.00000 + +#ifndef FLEXIBLE + +[ settles ] +; i funct doh dhh +1 1 0.09572 0.15139 + +#else +[ bonds ] +; i j funct length force.c. +1 2 1 0.09572 502416.0 0.09572 502416.0 +1 3 1 0.09572 502416.0 0.09572 502416.0 + +[ angles ] +; i j k funct angle force.c. +2 1 3 1 104.52 628.02 104.52 628.02 + +#endif + + +[ virtual_sites3 ] +; Vsite from funct a b +4 1 2 3 1 0.106676721 0.106676721 + + +[ exclusions ] +1 2 3 4 +2 1 3 4 +3 1 2 4 +4 1 2 3 + + +; The position of the virtual site is computed as follows: +; +; O +; +; V +; +; H H +; +; Ewald tip4p: +; const = distance (OV) / [ cos (angle(VOH)) * distance (OH) ] +; 0.0125 nm / [ cos (52.26 deg) * 0.09572 nm ] +; then a = b = 0.5 * const = 0.106676721 +; +; Vsite pos x4 = x1 + a*(x2-x1) + b*(x3-x1) diff --git a/pmx/data/mutff45/amber14sb_OL15.ff/tip5p.itp b/pmx/data/mutff45/amber14sb_OL15.ff/tip5p.itp new file mode 100644 index 00000000..c3120b0e --- /dev/null +++ b/pmx/data/mutff45/amber14sb_OL15.ff/tip5p.itp @@ -0,0 +1,69 @@ +[ moleculetype ] +; molname nrexcl +SOL 2 + +[ atoms ] +; id at type res nr res name at name cg nr charge mass + 1 OW_tip5p 1 SOL OW 1 0 16.00000 + 2 HW_tip5p 1 SOL HW1 1 0.241 1.00800 + 3 HW_tip5p 1 SOL HW2 1 0.241 1.00800 + 4 MW 1 SOL LP1 1 -0.241 0.00000 + 5 MW 1 SOL LP2 1 -0.241 0.00000 + +#ifndef FLEXIBLE + +[ settles ] +; i funct doh dhh +1 1 0.09572 0.15139 + +#else + +[ bonds ] +; i j funct length force.c. +1 2 1 0.09572 502416.0 0.09572 502416.0 +1 3 1 0.09572 502416.0 0.09572 502416.0 + +[ angles ] +; i j k funct angle force.c. +2 1 3 1 104.52 628.02 104.52 628.02 + +#endif + + +[ virtual_sites3 ] +; Vsite from funct a b c +4 1 2 3 4 -0.344908262 -0.34490826 -6.4437903493 +5 1 2 3 4 -0.344908262 -0.34490826 6.4437903493 + + +[ exclusions ] +1 2 3 4 5 +2 1 3 4 5 +3 1 2 4 5 +4 1 2 3 5 +5 1 2 3 4 + + +; The positions of the vsites are computed as follows: +; +; LP1 LP2 +; +; O +; +; H1 H2 +; +; angle A (H1-O-H2) = 104.52 +; angle B (M1-O-M2) = 109.47 +; dist C (H-O) = 0.09572 nm +; dist D (M-O) = 0.070 nm +; +;atom x y z +;O 0.0 0.0 0.0 +;H1 0.585882276 0.756950327 0.0 +;H2 0.585882276 -0.756950327 0.0 +;M1 -0.404151276 0.0 0.571543301 +;M2 -0.404151276 0.0 -0.571543301 +; Dummy pos x4 = x1 + a4*(x2-x1) + b4*(x3-x1) + c4*((x2-x1) x (x3-x1)) +; Dummy pos x5 = x1 + a5*(x2-x1) + b5*(x3-x1) + c5*((x2-x1) x (x3-x1)) +; a4 = b4 = a5 = b5 = (D*cos(B/2)) / (2*C*cos(A/2)) = -0.34490826 +; c5 = -c4 = (D * sin(B/2))/ (C^2 * sin(A)) = 6.4437903 diff --git a/pmx/data/mutff45/amber14sb_OL15.ff/urea.itp b/pmx/data/mutff45/amber14sb_OL15.ff/urea.itp new file mode 100644 index 00000000..8cb346a1 --- /dev/null +++ b/pmx/data/mutff45/amber14sb_OL15.ff/urea.itp @@ -0,0 +1,36 @@ +[ moleculetype ] +; molname nrexcl +URE 3 + +[ atoms ] + 1 C 1 URE C 1 0.880229 12.01000 ; amber C type + 2 O 1 URE O 2 -0.613359 16.00000 ; amber O type + 3 N 1 URE N1 3 -0.923545 14.01000 ; amber N type + 4 H 1 URE H11 4 0.395055 1.00800 ; amber H type + 5 H 1 URE H12 5 0.395055 1.00800 ; amber H type + 6 N 1 URE N2 6 -0.923545 14.01000 ; amber N type + 7 H 1 URE H21 7 0.395055 1.00800 ; amber H type + 8 H 1 URE H22 8 0.395055 1.00800 ; amber H type + +[ bonds ] + 1 2 + 1 3 + 1 6 + 3 4 + 3 5 + 6 7 + 6 8 +[ dihedrals ] +; ai aj ak al funct definition + 2 1 3 4 9 + 2 1 3 5 9 + 2 1 6 7 9 + 2 1 6 8 9 + 3 1 6 7 9 + 3 1 6 8 9 + 6 1 3 4 9 + 6 1 3 5 9 +[ dihedrals ] + 3 6 1 2 4 + 1 4 3 5 4 + 1 7 6 8 4 diff --git a/pmx/data/mutff45/amber14sb_OL15.ff/watermodels.dat b/pmx/data/mutff45/amber14sb_OL15.ff/watermodels.dat new file mode 100644 index 00000000..420808db --- /dev/null +++ b/pmx/data/mutff45/amber14sb_OL15.ff/watermodels.dat @@ -0,0 +1,5 @@ +tip3p TIP3P TIP 3-point, recommended +tip4p TIP4P TIP 4-point +tip4pew TIP4P-Ew TIP 4-point optimized with Ewald +spc SPC simple point charge +spce SPC/E extended simple point charge diff --git a/pmx/workflow/parmed/__init__.py b/pmx/workflow/parmed/__init__.py new file mode 100644 index 00000000..cbb3c59c --- /dev/null +++ b/pmx/workflow/parmed/__init__.py @@ -0,0 +1,87 @@ +""" +The parmed package manipulates molecular structures and provides a way to go +between standard and amber file formats, manipulate structures, etc. +""" + +# Version format should be "major.minor.patch". For beta releases, attach +# "-beta#" to the end. The beta number will be turned into another number in the +# version tuple +__author__ = 'Jason Swails' + +__all__ = ['exceptions', 'periodic_table', 'residue', 'unit', 'utils', 'Structure', 'entos', 'dlpoly', + 'StructureView', 'amber', 'charmm', 'namd', 'gromacs', 'tinker', 'openmm', 'rosetta', + 'rdkit', 'formats', 'Vec3', 'ParameterSet', 'load_file', 'read_PDB', 'read_CIF', + 'load_rosetta', 'load_rdkit', 'download_PDB', 'download_CIF', 'tools', 'version'] + +from ._version import get_versions +__version__ = get_versions()['version'] +del get_versions + +from . import exceptions, periodic_table, residue +from . import unit, utils +from .topologyobjects import * +from .structure import Structure, StructureView +from . import amber, charmm, gromacs, dlpoly, namd, openmm, rosetta, tinker, entos +from . import formats +from .vec3 import Vec3 +from .parameters import ParameterSet +from .rdkit import load_rdkit +from . import rdkit +load_file = formats.load_file +read_PDB = formats.PDBFile.parse +read_CIF = formats.CIFFile.parse +read_pdb = read_PDB +read_cif = read_CIF +load_rosetta = rosetta.RosettaPose.load + +download_PDB = formats.PDBFile.download +download_CIF = formats.CIFFile.download +download_pdb = download_PDB +download_cif = download_CIF + +# The tools package depends on *everything*, so import this at the end to avoid +# circular imports. +from . import tools # isort: skip + +# Add all of the objects from parmed.topologyobjects to the top-level namespace +__all__ += topologyobjects.__all__ + +# Build a version tuple from __version__ for easy comparison +import re +from collections import namedtuple +class version(namedtuple('version', ['major', 'minor', 'patchlevel', 'commits_ahead'])): + def __eq__(self, other): + try: + if len(other) == 3: + return (other == self[:3] and self.commits_ahead == 0 and not self.dirty) + except TypeError: + return NotImplemented + if tuple(other) != tuple(self): + return False + elif not hasattr(other, 'git_hash') or not hasattr(other, 'dirty'): + return not self.dirty + return self.git_hash == other.git_hash and self.dirty is other.dirty + + def __ne__(self, other): + return not self == other + + def __le__(self, other): + return self == other or self < other + + def __ge__(self, other): + return self == other or self > other + +# Build the version +_versionre = re.compile(r'(\d+)\.(\d+)\.(\d+)\+?(\d*)\.?g?([\dabcdefABCDEF]*)\.*(dirty)?') +if _versionre.match(__version__): + versionlist = list(_versionre.match(__version__).groups()) + versionlist[3] = versionlist[3] or 0 + version = version(*[int(v) for v in versionlist[:4]]) + version.git_hash = versionlist[4] + version.dirty = bool(versionlist[5]) +else: + versionlist = None + version = version(0, 0, 0, 0) + +# Clean up +del namedtuple, re, versionlist diff --git a/pmx/workflow/parmed/_version.py b/pmx/workflow/parmed/_version.py new file mode 100644 index 00000000..18cc1fed --- /dev/null +++ b/pmx/workflow/parmed/_version.py @@ -0,0 +1,498 @@ + +# This file helps to compute a version number in source trees obtained from +# git-archive tarball (such as those provided by githubs download-from-tag +# feature). Distribution tarballs (built by setup.py sdist) and build +# directories (produced by setup.py build) will contain a much shorter file +# that just contains the computed version number. + +# This file is released into the public domain. Generated by +# versioneer-0.16+dev (https://github.com/warner/python-versioneer) + +"""Git implementation of _version.py.""" + +import errno +import os +import re +import subprocess +import sys + + +def get_keywords(): + """Get the keywords needed to look up the version information.""" + # these strings will be replaced by git during git-archive. + # setup.py/versioneer.py will grep for the variable names, so they must + # each be defined on a line of their own. _version.py will just call + # get_keywords(). + git_refnames = "$Format:%d$" + git_full = "$Format:%H$" + keywords = {"refnames": git_refnames, "full": git_full} + return keywords + + +class VersioneerConfig: + """Container for Versioneer configuration parameters.""" + + +def get_config(): + """Create, populate and return the VersioneerConfig() object.""" + # these strings are filled in when 'setup.py versioneer' creates + # _version.py + cfg = VersioneerConfig() + cfg.VCS = "git" + cfg.style = "pep440" + cfg.tag_prefix = "'v'" + cfg.parentdir_prefix = "None" + cfg.versionfile_source = "parmed/_version.py" + cfg.verbose = False + return cfg + + +class NotThisMethod(Exception): + """Exception raised if a method is not valid for the current scenario.""" + + +LONG_VERSION_PY = {} +HANDLERS = {} + + +def register_vcs_handler(vcs, method): # decorator + """Decorator to mark a method as the handler for a particular VCS.""" + def decorate(f): + """Store f in HANDLERS[vcs][method].""" + if vcs not in HANDLERS: + HANDLERS[vcs] = {} + HANDLERS[vcs][method] = f + return f + return decorate + + +def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, + env=None): + """Call the given command(s).""" + assert isinstance(commands, list) + p = None + for c in commands: + try: + dispcmd = str([c] + args) + # remember shell=False, so use git.cmd on windows, not just git + p = subprocess.Popen([c] + args, cwd=cwd, env=env, + stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr + else None)) + break + except EnvironmentError: + e = sys.exc_info()[1] + if e.errno == errno.ENOENT: + continue + if verbose: + print("unable to run %s" % dispcmd) + print(e) + return None, None + else: + if verbose: + print("unable to find command, tried %s" % (commands,)) + return None, None + stdout = p.communicate()[0].strip() + if sys.version_info[0] >= 3: + stdout = stdout.decode() + if p.returncode != 0: + if verbose: + print("unable to run %s (error)" % dispcmd) + print("stdout was %s" % stdout) + return None, p.returncode + return stdout, p.returncode + + +def versions_from_parentdir(parentdir_prefix, root, verbose): + """Try to determine the version from the parent directory name. + + Source tarballs conventionally unpack into a directory that includes both + the project name and a version string. We will also support searching up + two directory levels for an appropriately named parent directory + """ + rootdirs = [] + + for i in range(3): + dirname = os.path.basename(root) + if dirname.startswith(parentdir_prefix): + return {"version": dirname[len(parentdir_prefix):], + "full-revisionid": None, + "dirty": False, "error": None} + else: + rootdirs.append(root) + root = os.path.dirname(root) # up a level + + if verbose: + print("Tried directories %s but none started with prefix %s" % + (str(rootdirs), parentdir_prefix)) + raise NotThisMethod("rootdir doesn't start with parentdir_prefix") + + +@register_vcs_handler("git", "get_keywords") +def git_get_keywords(versionfile_abs): + """Extract version information from the given file.""" + # the code embedded in _version.py can just fetch the value of these + # keywords. When used from setup.py, we don't want to import _version.py, + # so we do it with a regexp instead. This function is not used from + # _version.py. + keywords = {} + try: + f = open(versionfile_abs, "r") + for line in f.readlines(): + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["full"] = mo.group(1) + f.close() + except EnvironmentError: + pass + return keywords + + +@register_vcs_handler("git", "keywords") +def git_versions_from_keywords(keywords, tag_prefix, verbose): + """Get version information from git keywords.""" + if not keywords: + raise NotThisMethod("no keywords at all, weird") + refnames = keywords["refnames"].strip() + if refnames.startswith("$Format"): + if verbose: + print("keywords are unexpanded, not using") + raise NotThisMethod("unexpanded keywords, not a git-archive tarball") + refs = set([r.strip() for r in refnames.strip("()").split(",")]) + # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of + # just "foo-1.0". If we see a "tag: " prefix, prefer those. + TAG = "tag: " + tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) + if not tags: + # Either we're using git < 1.8.3, or there really are no tags. We use + # a heuristic: assume all version tags have a digit. The old git %d + # expansion behaves like git log --decorate=short and strips out the + # refs/heads/ and refs/tags/ prefixes that would let us distinguish + # between branches and tags. By ignoring refnames without digits, we + # filter out many common branch names like "release" and + # "stabilization", as well as "HEAD" and "master". + tags = set([r for r in refs if re.search(r'\d', r)]) + if verbose: + print("discarding '%s', no digits" % ",".join(refs-tags)) + if verbose: + print("likely tags: %s" % ",".join(sorted(tags))) + for ref in sorted(tags): + # sorting will prefer e.g. "2.0" over "2.0rc1" + if ref.startswith(tag_prefix): + r = ref[len(tag_prefix):] + if verbose: + print("picking %s" % r) + return {"version": r, + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": None + } + # no suitable tags, so version is "0+unknown", but full hex is still there + if verbose: + print("no suitable tags, using unknown + full revision id") + return {"version": "0+unknown", + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": "no suitable tags"} + + +@register_vcs_handler("git", "pieces_from_vcs") +def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): + """Get version from 'git describe' in the root of the source tree. + + This only gets called if the git-archive 'subst' keywords were *not* + expanded, and _version.py hasn't already been rewritten with a short + version string, meaning we're inside a checked out source tree. + """ + GITS = ["git"] + if sys.platform == "win32": + GITS = ["git.cmd", "git.exe"] + + out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, + hide_stderr=True) + if rc != 0: + if verbose: + print("Directory %s not under git control" % root) + raise NotThisMethod("'git rev-parse --git-dir' returned error") + + # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] + # if there isn't one, this yields HEX[-dirty] (no NUM) + describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", + "--always", "--long", + "--match", "%s*" % tag_prefix], + cwd=root) + # --long was added in git-1.5.5 + if describe_out is None: + raise NotThisMethod("'git describe' failed") + describe_out = describe_out.strip() + full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + if full_out is None: + raise NotThisMethod("'git rev-parse' failed") + full_out = full_out.strip() + + pieces = {} + pieces["long"] = full_out + pieces["short"] = full_out[:7] # maybe improved later + pieces["error"] = None + + # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] + # TAG might have hyphens. + git_describe = describe_out + + # look for -dirty suffix + dirty = git_describe.endswith("-dirty") + pieces["dirty"] = dirty + if dirty: + git_describe = git_describe[:git_describe.rindex("-dirty")] + + # now we have TAG-NUM-gHEX or HEX + + if "-" in git_describe: + # TAG-NUM-gHEX + mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) + if not mo: + # unparseable. Maybe git-describe is misbehaving? + pieces["error"] = ("unable to parse git-describe output: '%s'" + % describe_out) + return pieces + + # tag + full_tag = mo.group(1) + if not full_tag.startswith(tag_prefix): + if verbose: + fmt = "tag '%s' doesn't start with prefix '%s'" + print(fmt % (full_tag, tag_prefix)) + pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" + % (full_tag, tag_prefix)) + return pieces + pieces["closest-tag"] = full_tag[len(tag_prefix):] + + # distance: number of commits since tag + pieces["distance"] = int(mo.group(2)) + + # commit: short hex revision ID + pieces["short"] = mo.group(3) + + else: + # HEX: no tags + pieces["closest-tag"] = None + count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], + cwd=root) + pieces["distance"] = int(count_out) # total number of commits + + return pieces + + +def plus_or_dot(pieces): + """Return a + if we don't already have one, else return a .""" + if "+" in pieces.get("closest-tag", ""): + return "." + return "+" + + +def render_pep440(pieces): + """Build up version string, with post-release "local version identifier". + + Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you + get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty + + Exceptions: + 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += plus_or_dot(pieces) + rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0+untagged.%d.g%s" % (pieces["distance"], + pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def render_pep440_pre(pieces): + """TAG[.post.devDISTANCE] -- No -dirty. + + Exceptions: + 1: no tags. 0.post.devDISTANCE + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += ".post.dev%d" % pieces["distance"] + else: + # exception #1 + rendered = "0.post.dev%d" % pieces["distance"] + return rendered + + +def render_pep440_post(pieces): + """TAG[.postDISTANCE[.dev0]+gHEX] . + + The ".dev0" means dirty. Note that .dev0 sorts backwards + (a dirty tree will appear "older" than the corresponding clean one), + but you shouldn't be releasing software with -dirty anyways. + + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%s" % pieces["short"] + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += "+g%s" % pieces["short"] + return rendered + + +def render_pep440_old(pieces): + """TAG[.postDISTANCE[.dev0]] . + + The ".dev0" means dirty. + + Eexceptions: + 1: no tags. 0.postDISTANCE[.dev0] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + return rendered + + +def render_git_describe(pieces): + """TAG[-DISTANCE-gHEX][-dirty]. + + Like 'git describe --tags --dirty --always'. + + Exceptions: + 1: no tags. HEX[-dirty] (note: no 'g' prefix) + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render_git_describe_long(pieces): + """TAG-DISTANCE-gHEX[-dirty]. + + Like 'git describe --tags --dirty --always -long'. + The distance/hash is unconditional. + + Exceptions: + 1: no tags. HEX[-dirty] (note: no 'g' prefix) + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render(pieces, style): + """Render the given version pieces into the requested style.""" + if pieces["error"]: + return {"version": "unknown", + "full-revisionid": pieces.get("long"), + "dirty": None, + "error": pieces["error"]} + + if not style or style == "default": + style = "pep440" # the default + + if style == "pep440": + rendered = render_pep440(pieces) + elif style == "pep440-pre": + rendered = render_pep440_pre(pieces) + elif style == "pep440-post": + rendered = render_pep440_post(pieces) + elif style == "pep440-old": + rendered = render_pep440_old(pieces) + elif style == "git-describe": + rendered = render_git_describe(pieces) + elif style == "git-describe-long": + rendered = render_git_describe_long(pieces) + else: + raise ValueError("unknown style '%s'" % style) + + return {"version": rendered, "full-revisionid": pieces["long"], + "dirty": pieces["dirty"], "error": None} + + +def get_versions(): + """Get version information or return default if unable to do so.""" + # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have + # __file__, we can work backwards from there to the root. Some + # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which + # case we can only use expanded keywords. + + cfg = get_config() + verbose = cfg.verbose + + try: + return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, + verbose) + except NotThisMethod: + pass + + try: + root = os.path.realpath(__file__) + # versionfile_source is the relative path from the top of the source + # tree (where the .git directory might live) to this file. Invert + # this to find the root from __file__. + for i in cfg.versionfile_source.split('/'): + root = os.path.dirname(root) + except NameError: + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, + "error": "unable to find root of source tree"} + + try: + pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) + return render(pieces, cfg.style) + except NotThisMethod: + pass + + try: + if cfg.parentdir_prefix: + return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) + except NotThisMethod: + pass + + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, + "error": "unable to compute version"} diff --git a/pmx/workflow/parmed/amber/__init__.py b/pmx/workflow/parmed/amber/__init__.py new file mode 100644 index 00000000..c4b2439b --- /dev/null +++ b/pmx/workflow/parmed/amber/__init__.py @@ -0,0 +1,24 @@ +""" Package that provides an API to Amber-specific files """ + +__author__ = "Jason Swails " + +__all__ = ['AmberFormat', 'FortranFormat', 'AmberAsciiRestart', 'AmberMdcrd', + 'AmberMask', 'NetCDFTraj', 'NetCDFRestart', 'AmberOFFLibrary', + 'AmberParameterSet', 'AmberParm', 'ChamberParm', 'AmoebaParm', + 'Rst7', 'BeemanRestart', 'ConvertFromPSF', 'LoadParm', 'AMBERHOME', + 'titratable_residues'] + +from .amberformat import AmberFormat, FortranFormat +from .asciicrd import AmberAsciiRestart, AmberMdcrd +from .mask import AmberMask +from .netcdffiles import NetCDFTraj, NetCDFRestart +from .offlib import AmberOFFLibrary +from .parameters import AmberParameterSet +from .readparm import AmberParm, ChamberParm, AmoebaParm, Rst7, BeemanRestart, ConvertFromPSF, LoadParm +from . import titratable_residues + +# See if there is an AMBERHOME defined, which we will use by default. Otherwise, +# set it to the empty string +import os as _os +AMBERHOME = _os.getenv('AMBERHOME') or '' +del _os diff --git a/pmx/workflow/parmed/amber/_amberparm.py b/pmx/workflow/parmed/amber/_amberparm.py new file mode 100644 index 00000000..cdc8078b --- /dev/null +++ b/pmx/workflow/parmed/amber/_amberparm.py @@ -0,0 +1,2396 @@ +""" +This module contains an amber prmtop class that will read in all +parameters and allow users to manipulate that data and write a new +prmtop object. +""" +import copy as _copy +from collections import defaultdict +from math import sqrt +from warnings import warn + +import numpy as np + +from .. import unit as u +from ..constants import PrmtopPointers, TRUNCATED_OCTAHEDRON_ANGLE, RAD_TO_DEG, SMALL, DEG_TO_RAD +from ..exceptions import AmberError, AmberWarning, MoleculeError +from ..geometry import box_lengths_and_angles_to_vectors +from ..periodic_table import AtomicNum, element_by_mass +from ..residue import ALLION_NAMES, SOLVENT_NAMES +from ..structure import Structure, needs_openmm +from ..topologyobjects import (Angle, AngleType, Atom, AtomList, AtomType, + Bond, BondType, Dihedral, DihedralType, + DihedralTypeList, ExtraPoint, Cmap, CmapType) +from ..vec3 import Vec3 +from .amberformat import AmberFormat +from .asciicrd import AmberAsciiRestart +from .netcdffiles import NetCDFRestart + +try: + import openmm as mm + from openmm import app +except ImportError: + mm = app = None + +class AmberParm(AmberFormat, Structure): + """ + Amber Topology (parm7 format) class. Gives low, and some high, level access + to topology data. You can interact with the raw data in the topology file + directly or interact with some of the high-level classes comprising the + system topology and parameters. + + Parameters + ---------- + prm_name : str, optional + If provided, this file is parsed and the data structures will be loaded + from the data in this file + xyz : str or array, optional + If provided, the coordinates and unit cell dimensions from the provided + Amber inpcrd/restart file will be loaded into the molecule, or the + coordinates will be loaded from the coordinate array + box : array, optional + If provided, the unit cell information will be set from the provided + unit cell dimensions (a, b, c, alpha, beta, and gamma, respectively) + + Attributes + ---------- + parm_data : dict {str : list} + A dictionary that maps FLAG names to all of the data contained in that + section of the Amber file. + formats : dict {str : FortranFormat} + A dictionary that maps FLAG names to the FortranFormat instance in which + the data is stored in that section + parm_comments : dict {str : list} + A dictionary that maps FLAG names to the list of COMMENT lines that were + stored in the original file + flag_list : list + An ordered list of all FLAG names. This must be kept synchronized with + `parm_data`, `formats`, and `parm_comments` such that every item in + `flag_list` is a key to those 3 dicts and no other keys exist + charge_flag : str='CHARGE' + The name of the name of the FLAG that describes partial atomic charge + data. If this flag is found, then its data are multiplied by the + ELECTROSTATIC_CONSTANT to convert back to fractions of electrons + version : str + The VERSION string from the Amber file + name : str + The file name of the originally parsed file (set to the fname parameter) + LJ_types : dict {str : int} + A mapping whose keys are atom types paired with the nonbonded index of + that type + LJ_radius : list(float) + A list of floats corresponding to the Rmin/2 parameter for every + Lennard-Jones type. The indexes are the nonbonded index (`nb_idx` + attribute of the `Atom` class) minus 1 to account for indexing from 0 in + Python. The values are in Angstroms. To get the radius for a particular + atom type, you can use `LJ_radius[LJ_types["type"]-1]` + LJ_depth : list(float) + A list of Lennard-Jones epsilon parameters laid out the same way as + LJ_radius, described above. + atoms : AtomList(Atom) + List of all atoms in the system + residues : ResidueList(Residue) + List of all residues in the system + bonds : TrackedList(Bond) + List of bonds between two atoms in the system + angles : TrackedList(Angle) + List of angles between three atoms in the system + dihedrals : TrackedList(Angle) + List of all proper and improper torsions between 4 atoms in the system + box : list of 6 floats + Periodic boundary unit cell dimensions and angles + bond_types : TrackedList(BondType) + The bond types containing the parameters for each bond stretching term + angle_types : TrackedList(AngleType) + The angle types containing the parameters for each angle bending term + dihedral_types : TrackedList(DihedralType) + The dihedral types containing the parameters for each torsional term + bonds_inc_h : iterator(Bond) + Read-only generator that loops through all bonds that contain Hydrogen + bonds_without_h : iterator(Bond) + Read-only generator that loops through all bonds that do not contain + Hydrogen + angles_inc_h : iterator(Angle) + Read-only generator that loops through all angles that contain Hydrogen + angles_without_h : iterator(Angle) + Read-only generator that loops through all angles that do not contain + Hydrogen + dihedrals_inc_h : iterator(Dihedral) + Read-only generator that loops through all dihedrals that contain + Hydrogen + dihedrals_without_h : iterator(Dihedral) + Read-only generator that loops through all dihedrals that do not contain + Hydrogen + chamber : bool=False + On AmberParm instances, this is always False to indicate that it is not + a CHAMBER-style topology file + amoeba : bool=False + On AmberParm instances, this is always False to indicate that it is not + an AMOEBA-style topology file + has_cmap : bool=False + On AmberParm instances, this is always False to indicate that it does + not have correction maps (unique to CHARMM force field and chamber + topologies) + """ + + _cmap_prefix = "" + + #=================================================== + + def __init__(self, prm_name=None, xyz=None, box=None): + """ + Instantiates an AmberParm object from data in prm_name and establishes + validity based on presence of POINTERS and CHARGE sections. In general, + you should use LoadParm from the readparm module instead. LoadParm will + correctly dispatch the object to the 'correct' flavor of AmberParm + """ + AmberFormat.__init__(self, prm_name) + Structure.__init__(self) + self.hasvels = False + self.hasbox = False + self._box = None + self.crdname = None + if isinstance(xyz, str): + self.crdname = xyz + if prm_name is not None: + self.initialize_topology(xyz, box) + + #=================================================== + + def initialize_topology(self, xyz=None, box=None): + """ + Initializes topology data structures, like the list of atoms, bonds, + etc., after the topology file has been read. + """ + from .. import load_file + # We need to handle RESIDUE_ICODE properly since it may have picked up + # some extra values + if 'RESIDUE_ICODE' in self.flag_list: + self._truncate_array('RESIDUE_ICODE', self.parm_data['POINTERS'][PrmtopPointers.NRES]) + # Set up some of the attributes provided + self.pointers = {} + self.LJ_types = {} + self.LJ_radius = [] + self.LJ_depth = [] + self.load_pointers() + self.fill_LJ() + # The way we check if the combining rules are geometric vs. + # Lorentz-Berthelot is to try and change the combining rules to + # geometric *if we detect NBFIXes* and see if we still have NBFIXes. If + # not, then our combining rules are clearly geometric. + if not self.has_1012() and self.has_NBFIX(): + self.combining_rule = 'geometric' + if self.has_NBFIX(): + self.combining_rule = 'lorentz' + # Instantiate the Structure data structures + self.load_structure() + + if isinstance(xyz, str): + f = load_file(xyz, skip_bonds=True) + if not hasattr(f, 'coordinates') or f.coordinates is None: + raise TypeError('%s does not have coordinates' % xyz) + self.coordinates = f.coordinates + if hasattr(f, 'velocities') and f.velocities is not None: + self.velocities = f.velocities + if hasattr(f, 'box') and f.box is not None and box is None: + self.box = f.box + else: + self.coordinates = xyz + if box is not None: + self.box = box + + # If all else fails, set the box from the prmtop file + if self.parm_data['POINTERS'][PrmtopPointers.IFBOX] > 0 and self.box is None: + box = self.parm_data['BOX_DIMENSIONS'] + self.box = list(box[1:]) + [box[0], box[0], box[0]] + + self.hasbox = self.box is not None + + #=================================================== + + @classmethod + def from_rawdata(cls, rawdata): + """ + Take the raw data from a AmberFormat object and initialize an AmberParm + from that data. + + Parameters + ---------- + rawdata : :class:`AmberFormat` + An AmberFormat instance that has already been instantiated + + Returns + ------- + parm : :class:`AmberParm` + An instance of this type from the data in rawdata + """ + inst = cls() + inst.name = rawdata.name + inst.version = rawdata.version + inst.formats = rawdata.formats + inst.parm_data = rawdata.parm_data + inst.parm_comments = rawdata.parm_comments + inst.flag_list = rawdata.flag_list + inst.initialize_topology() + # See if the rawdata has any kind of structural attributes, like + # coordinates and an atom list with positions and/or velocities + if hasattr(rawdata, 'coordinates'): + inst.coordinates = _copy.copy(rawdata.coordinates) + if hasattr(rawdata, 'velocities'): + inst.velocities = _copy.copy(rawdata.velocities) + if hasattr(rawdata, 'box'): + inst.box = _copy.copy(rawdata.box) + inst.hasbox = inst.box is not None + inst.hasvels = inst.velocities is not None + n_copy = inst.pointers.get('NCOPY', 1) + if n_copy >= 2: + inst._label_alternates() + return inst + + #=================================================== + + @classmethod + def from_structure(cls, struct, copy=False): + """ + Take a Structure instance and initialize an AmberParm instance from that + data. + + Parameters + ---------- + struct : :class:`Structure` + The input structure from which to construct an AmberParm instance + copy : bool + If True, the input struct is deep-copied to make sure it does not + share any objects with the original ``struct``. Default is False + + Returns + ------- + inst : :class:`AmberParm` + The AmberParm instance derived from the input structure + + Raises + ------ + TypeError + If the structure has parameters not supported by the standard Amber + force field (i.e., standard bond, angle, and dihedral types) + + Notes + ----- + Due to the nature of the prmtop file, struct almost *always* returns a + deep copy. The one exception is when struct is already of type + :class:`AmberParm`, in which case the original object is returned unless + ``copy`` is ``True``. + """ + if type(struct) is cls: + if copy: + return _copy.copy(struct) + return struct + if struct.unknown_functional: + raise TypeError('Cannot instantiate an AmberParm from unknown functional') + if (struct.urey_bradleys or struct.impropers or + struct.trigonal_angles or struct.pi_torsions or + struct.out_of_plane_bends or struct.stretch_bends or + struct.torsion_torsions or struct.multipole_frames): + if (struct.trigonal_angles or struct.pi_torsions or + struct.out_of_plane_bends or struct.torsion_torsions or + struct.multipole_frames or struct.stretch_bends): + raise TypeError('AmberParm does not support all of the ' + 'parameters defined in the input Structure') + # Maybe it just has CHARMM parameters? + raise TypeError('AmberParm does not support all of the parameters ' + 'defined in the input Structure. Try ChamberParm') + + # Convert all RB torsions to propers and delete the originals. + for dihedral in struct.rb_torsions: + proper_types = DihedralTypeList.from_rbtorsion(dihedral.type) + for proper_type in proper_types: + proper = _copy.copy(dihedral) + proper.type = proper_type + struct.dihedrals.append(proper) + struct.dihedral_types.append(proper.type) + + del struct.rb_torsions[:] + del struct.rb_torsion_types[:] + struct.dihedrals.claim() + struct.dihedral_types.claim() + + inst = struct.copy(cls, split_dihedrals=True) + inst.update_dihedral_exclusions() + inst._add_missing_13_14() + del inst.adjusts[:] + inst.pointers = {} + inst.LJ_types = {} + nbfixes = inst._set_nbidx_lj_params() + inst._add_standard_flags() + inst.pointers['NATOM'] = len(inst.atoms) + inst.parm_data['POINTERS'][PrmtopPointers.NATOM] = len(inst.atoms) + # pmemd likes to skip torsions with periodicities of 0, which may be + # present as a way to hack entries into the 1-4 pairlist. See + # https://github.com/ParmEd/ParmEd/pull/145 for discussion. The solution + # here is to simply set that periodicity to 1. + inst._cleanup_dihedrals_with_periodicity_zero() + inst.remake_parm() + inst._set_nonbonded_tables(nbfixes) + n_copy = inst.pointers.get('NCOPY', 1) + if n_copy >= 2: + inst._label_alternates() + + # Setting box sets some of the flag data appropriately + inst.box = inst.get_box() + return inst + + #=================================================== + + def _set_nbidx_lj_params(self): + nbfixes = self.atoms.assign_nbidx_from_types() + # Give virtual sites a name that Amber understands + for atom in self.atoms: + atom.type = atom.type if not isinstance(atom, ExtraPoint) else 'EP' + # Fill the Lennard-Jones arrays/dicts + ntyp = 0 + for atom in self.atoms: + self.LJ_types[atom.type] = atom.nb_idx + ntyp = max(ntyp, atom.nb_idx) + self.LJ_radius = [0 for i in range(ntyp)] + self.LJ_depth = [0 for i in range(ntyp)] + for atom in self.atoms: + self.LJ_radius[atom.nb_idx-1] = atom.atom_type.rmin + self.LJ_depth[atom.nb_idx-1] = atom.atom_type.epsilon + return nbfixes + + #=================================================== + + def __copy__(self): + """ Needs to copy a few additional data structures """ + other = super(AmberParm, self).__copy__() + other.initialize_topology() + # Copy coordinates, box, etc. if applicable + other.coordinates = _copy.copy(self.get_coordinates('all')) + other._box = _copy.copy(self._box) + # Now we should have a full copy + return other + + #=================================================== + + def __getitem__(self, selection): + other = super(AmberParm, self).__getitem__(selection) + if isinstance(other, Atom): + return other + other.pointers = {} + self._copy_lj_data(other) + other._add_standard_flags() + other.remake_parm() + other._set_nonbonded_tables() + # Add back the original L-J tables to restore any NBFIXes that may have + # been there + other.parm_data['LENNARD_JONES_ACOEF'] = self.parm_data['LENNARD_JONES_ACOEF'][:] + other.parm_data['LENNARD_JONES_BCOEF'] = self.parm_data['LENNARD_JONES_BCOEF'][:] + if 'LENNARD_JONES_CCOEF' in self.parm_data: + other.parm_data['LENNARD_JONES_CCOEF'] = self.parm_data['LENNARD_JONES_CCOEF'][:] + return other + + def _copy_lj_data(self, other): + """ Copies Lennard-Jones lists and dicts from myself to a copy """ + other.LJ_types = self.LJ_types.copy() + other.LJ_radius = _copy.copy(self.LJ_radius) + other.LJ_depth = _copy.copy(self.LJ_depth) + for atom in other.atoms: + other.LJ_radius[atom.nb_idx-1] = atom.atom_type.rmin + other.LJ_depth[atom.nb_idx-1] = atom.atom_type.epsilon + + #=================================================== + + def __imul__(self, ncopies, other=None): + super(AmberParm, self).__imul__(ncopies, other) + self.remake_parm() + return self + + def __iadd__(self, other): + if self.has_NBFIX() or self.has_1012(): + raise ValueError('Cannot combine Amber systems with NBFIX or 10-12 parameters') + super(AmberParm, self).__iadd__(other) + # Make sure we properly recompute the LJ tables + nbfixes = self._set_nbidx_lj_params() + self.remake_parm() + self._set_nonbonded_tables(nbfixes) + return self + + #=================================================== + + def load_pointers(self): + """ + Loads the data in POINTERS section into a pointers dictionary with each + key being the pointer name according to http://ambermd.org/formats.html + """ + self.pointers["NATOM"] = self.parm_data["POINTERS"][PrmtopPointers.NATOM] + self.pointers["NTYPES"] = self.parm_data["POINTERS"][PrmtopPointers.NTYPES] + self.pointers["NBONH"] = self.parm_data["POINTERS"][PrmtopPointers.NBONH] + self.pointers["MBONA"] = self.parm_data["POINTERS"][PrmtopPointers.MBONA] + self.pointers["NTHETH"] = self.parm_data["POINTERS"][PrmtopPointers.NTHETH] + self.pointers["MTHETA"] = self.parm_data["POINTERS"][PrmtopPointers.MTHETA] + self.pointers["NPHIH"] = self.parm_data["POINTERS"][PrmtopPointers.NPHIH] + self.pointers["MPHIA"] = self.parm_data["POINTERS"][PrmtopPointers.MPHIA] + self.pointers["NHPARM"] = self.parm_data["POINTERS"][PrmtopPointers.NHPARM] + self.pointers["NPARM"] = self.parm_data["POINTERS"][PrmtopPointers.NPARM] + self.pointers["NEXT"] = self.parm_data["POINTERS"][PrmtopPointers.NEXT] + self.pointers["NNB"] = self.parm_data["POINTERS"][PrmtopPointers.NNB] # alias for above + self.pointers["NRES"] = self.parm_data["POINTERS"][PrmtopPointers.NRES] + self.pointers["NBONA"] = self.parm_data["POINTERS"][PrmtopPointers.NBONA] + self.pointers["NTHETA"] = self.parm_data["POINTERS"][PrmtopPointers.NTHETA] + self.pointers["NPHIA"] = self.parm_data["POINTERS"][PrmtopPointers.NPHIA] + self.pointers["NUMBND"] = self.parm_data["POINTERS"][PrmtopPointers.NUMBND] + self.pointers["NUMANG"] = self.parm_data["POINTERS"][PrmtopPointers.NUMANG] + self.pointers["NPTRA"] = self.parm_data["POINTERS"][PrmtopPointers.NPTRA] + self.pointers["NATYP"] = self.parm_data["POINTERS"][PrmtopPointers.NATYP] + self.pointers["NPHB"] = self.parm_data["POINTERS"][PrmtopPointers.NPHB] + self.pointers["IFPERT"] = self.parm_data["POINTERS"][PrmtopPointers.IFPERT] + self.pointers["NBPER"] = self.parm_data["POINTERS"][PrmtopPointers.NBPER] + self.pointers["NGPER"] = self.parm_data["POINTERS"][PrmtopPointers.NGPER] + self.pointers["NDPER"] = self.parm_data["POINTERS"][PrmtopPointers.NDPER] + self.pointers["MBPER"] = self.parm_data["POINTERS"][PrmtopPointers.MBPER] + self.pointers["MGPER"] = self.parm_data["POINTERS"][PrmtopPointers.MGPER] + self.pointers["MDPER"] = self.parm_data["POINTERS"][PrmtopPointers.MDPER] + self.pointers["IFBOX"] = self.parm_data["POINTERS"][PrmtopPointers.IFBOX] + self.pointers["NMXRS"] = self.parm_data["POINTERS"][PrmtopPointers.NMXRS] + self.pointers["IFCAP"] = self.parm_data["POINTERS"][PrmtopPointers.IFCAP] + self.pointers["NUMEXTRA"] = self.parm_data["POINTERS"][PrmtopPointers.NUMEXTRA] + if self.parm_data['POINTERS'][PrmtopPointers.IFBOX] > 0: + self.pointers['IPTRES'] = self.parm_data['SOLVENT_POINTERS'][0] + self.pointers['NSPM'] = self.parm_data['SOLVENT_POINTERS'][1] + self.pointers['NSPSOL'] = self.parm_data['SOLVENT_POINTERS'][2] + # The next is probably only there for LES-prmtops + try: + self.pointers["NCOPY"] = self.parm_data["POINTERS"][PrmtopPointers.NCOPY] + except (KeyError, IndexError): + pass + # If CMAP is not present, don't load the pointers + if self.has_cmap: + self.pointers['CMAP'] = self.parm_data[f"{self._cmap_prefix}CMAP_COUNT"][0] + self.pointers['CMAP_TYPES'] = self.parm_data[f"{self._cmap_prefix}CMAP_COUNT"][1] + + #=================================================== + + def load_structure(self): + """ + Loads all of the topology instance variables. This is necessary if we + actually want to modify the topological layout of our system + (like deleting atoms) + """ + self._check_section_lengths() + self._load_atoms_and_residues() + self.load_atom_info() + self._load_bond_info() + self._load_angle_info() + self._load_dihedral_info() + self._load_cmap_info() + self._load_extra_exclusions() + super().unchange() + + #=================================================== + + def load_atom_info(self): + """ + Loads atom properties into the atoms that have been loaded. If any + arrays are too short or too long, an IndexError will be raised + """ + # Collect all of the atom properties present in our topology file + zeros = _zeros(len(self.atoms)) + anam = self.parm_data['ATOM_NAME'] + chg = self.parm_data['CHARGE'] + mass = self.parm_data['MASS'] + nbtyp = self.parm_data['ATOM_TYPE_INDEX'] + atyp = self.parm_data['AMBER_ATOM_TYPE'] + join = self.parm_data['JOIN_ARRAY'] + irot = self.parm_data['IROTAT'] + tree = self.parm_data['TREE_CHAIN_CLASSIFICATION'] + try: + radii = self.parm_data['RADII'] + except KeyError: + radii = zeros + try: + screen = self.parm_data['SCREEN'] + except KeyError: + screen = zeros + try: + atnum = self.parm_data['ATOMIC_NUMBER'] + replace_atnum = True + except KeyError: + atnum = [AtomicNum[element_by_mass(m)] for m in mass] + replace_atnum = False + try: + occu = self.parm_data['ATOM_OCCUPANCY'] + except KeyError: + occu = zeros + try: + bfac = self.parm_data['ATOM_BFACTOR'] + except KeyError: + bfac = zeros + try: + anum = self.parm_data['ATOM_NUMBER'] + except KeyError: + anum = [-1 for atom in self.atoms] + for i, atom in enumerate(self.atoms): + atom.name = anam[i] + atom.charge = chg[i] + atom.mass = mass[i] + atom.nb_idx = nbtyp[i] + atom.type = atyp[i] + atom.join = join[i] + atom.irotat = irot[i] + atom.tree = tree[i] + atom.solvent_radius = radii[i] + atom.screen = screen[i] + if replace_atnum or atom.atomic_number == 0: + if atnum[i] == -1: + atom.atomic_number = AtomicNum[element_by_mass(mass[i])] + else: + atom.atomic_number = atnum[i] + atom.atom_type = AtomType(atyp[i], None, mass[i], atnum[i]) + atom.occupancy = occu[i] + atom.bfactor = bfac[i] + atom.number = anum[i] + depth = self.LJ_depth[atom.nb_idx-1] + radius = self.LJ_radius[atom.nb_idx-1] + try: + depth14 = self.LJ_14_depth[atom.nb_idx-1] + radius14 = self.LJ_14_radius[atom.nb_idx-1] + except AttributeError: + depth14 = radius14 = None + atom.atom_type.set_lj_params(depth, radius, depth14, radius14) + + #=================================================== + + def ptr(self, pointer: str) -> int: + """ + Returns the value of the given pointer, and converts to upper-case so + it's case-insensitive. A non-existent pointer meets with a KeyError + + Parameters + ---------- + pointer : str + The AMBER pointer for which to extract the value + + Returns + ------- + int + The returned integer is the value of that pointer + """ + return self.pointers[pointer.upper()] + + #=================================================== + + def write_rst7(self, name, netcdf=None): + """ + Writes a restart file with the current coordinates and velocities and + box info if it's present + + Parameters + ---------- + name : str + Name of the file to write the restart file to + netcdf : bool=False + If True, write a NetCDF-format restart file (requires a NetCDF + backend; scipy, netCDF4, or ScientificPython; to be installed) + + Notes + ----- + If `netcdf` is not specified and the filename extension given by `name` + is `.ncrst`, the a NetCDF restart file will be written. However, an + explicit value for `netcdf` will override any filename extensions. + """ + # By default, determine file type by extension (.ncrst is NetCDF) + netcdf = netcdf or (netcdf is None and name.endswith('.ncrst')) + + # Check that we have a rst7 loaded, then overwrite it with a new one if + # necessary + rst7 = Rst7(natom=len(self.atoms)) + + # Now fill in the rst7 coordinates + rst7.coordinates = [0.0 for i in range(len(self.atoms)*3)] + if self.velocities is not None: + rst7.vels = [0.0 for i in range(len(self.atoms)*3)] + + for i, at in enumerate(self.atoms): + i3 = i * 3 + rst7.coordinates[i3 ] = at.xx + rst7.coordinates[i3+1] = at.xy + rst7.coordinates[i3+2] = at.xz + if rst7.hasvels: + rst7.vels[i3 ] = at.vx + rst7.vels[i3+1] = at.vy + rst7.vels[i3+2] = at.vz + + rst7.box = _copy.copy(self.box) + # Now write the restart file + rst7.write(name, netcdf) + + #=================================================== + + def write_parm(self, name): + """ + Writes the current data in parm_data into a new topology file with a given name. + + Parameters + ---------- + name : str or file-like + The name of the file to write the prmtop to or the file object to write to + """ + self.remake_parm() + AmberFormat.write_parm(self, name) + + #=================================================== + + def remake_parm(self): + """ + Fills :attr:`parm_data` from the data in the parameter and topology + arrays (e.g., :attr:`atoms`, :attr:`bonds`, :attr:`bond_types`, ...) + """ + # Get rid of terms containing deleted atoms and empty residues + self.prune_empty_terms() + self.residues.prune() + self.rediscover_molecules() + + # Transfer information from the topology lists + self._xfer_atom_info() + self._xfer_residue_info() + self._xfer_bond_info() + self._xfer_angle_info() + self._xfer_dihedral_info() + self._xfer_cmap_properties() + self._set_ifbox() + # Load the pointers dict + self.load_pointers() + # Mark atom list as unchanged + super().unchange() + + #=================================================== + + def is_changed(self): + """ + Determines if any of the topological arrays have changed since the + last upload + """ + is_changed = super(AmberParm, self).is_changed() + if is_changed and hasattr(self, '_topology'): + del self._topology + return is_changed + + #=================================================== + + def strip(self, selection): + """ + Deletes a subset of the atoms corresponding to an atom-based selection. + + Parameters + ---------- + selection : AmberMask, str, or iterable of bool + This is the selection of atoms that will be deleted from this + structure. If it is a string, it will be interpreted as an + AmberMask. If it is an AmberMask, it will be converted to a + selection of atoms. If it is an iterable, it must be the same length + as the `atoms` list. + """ + super(AmberParm, self).strip(selection) + self.remake_parm() + + #=================================================== + + def rediscover_molecules(self, solute_ions=True, fix_broken=True): + """ + This determines the molecularity and sets the ATOMS_PER_MOLECULE and + SOLVENT_POINTERS sections of the prmtops. Returns the new atom sequence + in terms of the 'old' atom indexes if re-ordering was necessary to fix + the tleap bug. Returns None otherwise. + """ + from ..utils import tag_molecules + # Bail out of we are not doing a solvated prmtop + if self.parm_data['POINTERS'][PrmtopPointers.IFBOX] == 0 or self.box is None: + return None + + owner = tag_molecules(self) + all_solvent = SOLVENT_NAMES + if not solute_ions: + all_solvent = all_solvent | ALLION_NAMES + first_solvent = None + for i, res in enumerate(self.residues): + if res.name in all_solvent: + first_solvent = i + break + # Now remake our SOLVENT_POINTERS and ATOMS_PER_MOLECULE section + self.parm_data['SOLVENT_POINTERS'] = [first_solvent, len(owner), 0] + # Find the first solvent molecule if there are any + if first_solvent is None: + self.parm_data['SOLVENT_POINTERS'][0] = len(self.residues) + self.parm_data['SOLVENT_POINTERS'][2] = len(owner) + 1 + else: + self.parm_data['SOLVENT_POINTERS'][2] = self.residues[first_solvent].atoms[0].marked + + # Now set up ATOMS_PER_MOLECULE and catch any errors + self.parm_data['ATOMS_PER_MOLECULE'] = [len(mol) for mol in owner] + + # Check that all of our molecules are contiguous, because we have to + # re-order atoms if they're not + try: + for mol in owner: + sortedmol = sorted(list(mol)) + for i in range(1, len(sortedmol)): + if sortedmol[i] != sortedmol[i-1] + 1: + raise StopIteration() + except StopIteration: + if not fix_broken: + raise MoleculeError('Molecule atoms are not contiguous!') + # Non-contiguous molecules detected... time to fix (ugh!) + warn('Molecule atoms are not contiguous! Attempting to reorder to fix.', AmberWarning) + # Make sure that no residues are split up by this + for res in self.residues: + molid = res.atoms[0].marked + for atom in res: + if molid != atom.marked: + warn('Residues cannot be part of 2 molecules! Molecule ' + 'section will not be correctly set. [Offending ' + f'residue is {repr(res)}', AmberWarning) + return None + new_atoms = AtomList() + for mol in owner: + for idx in sorted(mol): + new_atoms.append(self.atoms[idx]) + self.atoms = new_atoms + # Re-sort our residues and residue list for new atom ordering + for residue in self.residues: + residue.sort() + self.residues.sort() + return owner + + return None + + #=================================================== + + def fill_LJ(self): + """ + Calculates the Lennard-Jones parameters (Rmin/2 and epsilon) for each + atom type by computing their values from the A and B coefficients of + each atom interacting with itself. + + This fills the :attr:`LJ_radius`, :attr:`LJ_depth`, and :attr:`LJ_types` + data structures. + """ + self.LJ_radius = [] # empty LJ_radii so it can be re-filled + self.LJ_depth = [] # empty LJ_depths so it can be re-filled + self.LJ_types = {} # empty LJ_types so it can be re-filled + one_sixth = 1 / 6 # we need to raise some numbers to the 1/6th power + + pd = self.parm_data + acoef = pd['LENNARD_JONES_ACOEF'] + bcoef = pd['LENNARD_JONES_BCOEF'] + natom = self.pointers['NATOM'] + ntypes = self.pointers['NTYPES'] + for i in range(natom): # fill the LJ_types array + self.LJ_types[pd["AMBER_ATOM_TYPE"][i]] = pd["ATOM_TYPE_INDEX"][i] + + for i in range(ntypes): + lj_index = pd["NONBONDED_PARM_INDEX"][ntypes*i+i] - 1 + if lj_index < 0 or acoef[lj_index] < 1.0e-10 or bcoef[lj_index] < 1.0e-10: + self.LJ_radius.append(0) + self.LJ_depth.append(0) + else: + factor = 2 * acoef[lj_index] / bcoef[lj_index] + self.LJ_radius.append(pow(factor, one_sixth) * 0.5) + self.LJ_depth.append(bcoef[lj_index] / 2 / factor) + + #=================================================== + + def recalculate_LJ(self): + """ + Fills the ``LENNARD_JONES_ACOEF`` and ``LENNARD_JONES_BCOEF`` arrays in + the :attr:`parm_data` raw data dictionary by applying the canonical + Lorentz-Berthelot combining rules to the values in :attr:`LJ_radius` and + :attr:`LJ_depth`. + + Notes + ----- + This will undo any off-diagonal L-J modifications you may have made, so + call this function with care. + """ + assert self.combining_rule in ('lorentz', 'geometric'), "Unrecognized combining rule" + if self.combining_rule == 'lorentz': + comb_sig = lambda sig1, sig2: 0.5 * (sig1 + sig2) + elif self.combining_rule == 'geometric': + comb_sig = lambda sig1, sig2: sqrt(sig1 * sig2) + pd = self.parm_data + ntypes = self.pointers['NTYPES'] + fac = 2**(-1/6) * 2 + LJ_sigma = [x*fac for x in self.LJ_radius] + fac = 2**(1/6) + for i in range(ntypes): + for j in range(i, ntypes): + index = pd['NONBONDED_PARM_INDEX'][ntypes*i+j] - 1 + if index < 0: + # This indicates *either* a 10-12 potential for this pair + # _or_ it indicates a placeholder for HW atom type + # interactions and serves as a tag for fast water routines + # (or did in the past, anyway). So just skip to the next one + continue + rij = comb_sig(LJ_sigma[i], LJ_sigma[j]) * fac + wdij = sqrt(self.LJ_depth[i] * self.LJ_depth[j]) + pd["LENNARD_JONES_ACOEF"][index] = wdij * rij**12 + pd["LENNARD_JONES_BCOEF"][index] = 2 * wdij * rij**6 + + #=================================================== + + def has_NBFIX(self): + """ + This routine determines whether there are any off-diagonal Lennard-Jones + modifications (i.e., if any two atoms have a L-J pair interaction that + does not match the combined L-J parameters for that pair). + + Returns + ------- + nbfix : bool + If True, off-diagonal elements in the combined Lennard-Jones matrix + exist. If False, they do not. + """ + assert self.combining_rule in ('lorentz', 'geometric'), "Unrecognized combining rule" + if self.combining_rule == 'lorentz': + comb_sig = lambda sig1, sig2: 0.5 * (sig1 + sig2) + elif self.combining_rule == 'geometric': + comb_sig = lambda sig1, sig2: sqrt(sig1 * sig2) + fac = 2**(-1/6) * 2 + LJ_sigma = [x*fac for x in self.LJ_radius] + pd = self.parm_data + ntypes = self.parm_data['POINTERS'][PrmtopPointers.NTYPES] + fac = 2**(1/6) + for i in range(ntypes): + for j in range(ntypes): + idx = pd['NONBONDED_PARM_INDEX'][ntypes*i+j] - 1 + if idx < 0: continue + rij = comb_sig(LJ_sigma[i], LJ_sigma[j]) * fac + wdij = sqrt(self.LJ_depth[i] * self.LJ_depth[j]) + a = pd['LENNARD_JONES_ACOEF'][idx] + b = pd['LENNARD_JONES_BCOEF'][idx] + if a == 0 or b == 0: + if a != 0 or b != 0 or (wdij != 0 and rij != 0): + return True + elif (abs((a - (wdij * rij**12)) / a) > 1e-6 or + abs((b - (2 * wdij * rij**6)) / b) > 1e-6): + return True + return False + + #=================================================== + + def has_1012(self): + """ + This routine determines whether there are any defined 10-12 + Lennard-Jones interactions that are non-zero + + Returns + ------- + has_10_12 : bool + If True, 10-12 interactions *are* defined for this particular system + """ + indices_with_1012 = [] + ntypes = self.parm_data['POINTERS'][PrmtopPointers.NTYPES] + for i in range(ntypes): + for j in range(ntypes): + idx = self.parm_data['NONBONDED_PARM_INDEX'][i*ntypes+j] - 1 + if idx >= 0: continue + # It was negative, so we should have ADDED 1 to adjust for + # indexing from 0 + idx = -idx - 2 + a = self.parm_data['HBOND_ACOEF'][idx] + b = self.parm_data['HBOND_BCOEF'][idx] + if a == 0 and b == 0: continue + indices_with_1012.append((i, j)) + if not indices_with_1012: return False + # Now make sure that some of the atoms *have* those indices + active_indices = set() + for atom in self.atoms: active_indices.add(atom.nb_idx-1) + for i, j in indices_with_1012: + if i in active_indices and j in active_indices: + return True + return False + + #=================================================== + + def load_rst7(self, rst7): + """ Loads coordinates into the AmberParm class + + Parameters + ---------- + rst7 : str or :class:`Rst7` + The Amber coordinate file (either ASCII restart or NetCDF restart) + object or filename to assign atomic coordinates from. + """ + if not hasattr(rst7, 'coordinates'): + rst7 = Rst7.open(rst7) + self.coordinates = rst7.coordinates + self.hasvels = rst7.hasvels + self.box = _copy.copy(rst7.box) + self.hasbox = self.box is not None + if self.hasvels: + self.velocities = rst7.vels + + #=================================================== + + @needs_openmm + def omm_nonbonded_force(self, nonbondedMethod=None, + nonbondedCutoff=8*u.angstroms, + switchDistance=0*u.angstroms, + ewaldErrorTolerance=0.0005, + reactionFieldDielectric=78.5): + """ + Creates the OpenMM NonbondedForce (and CustomNonbondedForce if + necessary) to define the nonbonded interatomic potential for this + system. A CustomNonbondedForce is used for the r^-4 part of the 12-6-4 + Lennard-Jones potential as well as any modified off-diagonal (i.e., + NBFIX) terms + + Parameters + ---------- + nonbondedMethod : cutoff method + This is the cutoff method. It can be either the NoCutoff, + CutoffNonPeriodic, CutoffPeriodic, PME, or Ewald objects from the + openmm.app namespace + nonbondedCutoff : float or distance Quantity + The nonbonded cutoff must be either a floating point number + (interpreted as nanometers) or a Quantity with attached units. This + is ignored if nonbondedMethod is NoCutoff. + switchDistance : float or distance Quantity + The distance at which the switching function is turned on for van + der Waals interactions. This is ignored when no cutoff is used, and + no switch is used if switchDistance is 0, negative, or greater than + the cutoff + ewaldErrorTolerance : float=0.0005 + When using PME or Ewald, the Ewald parameters will be calculated + from this value + reactionFieldDielectric : float=78.5 + If the nonbondedMethod is CutoffPeriodic or CutoffNonPeriodic, the + region beyond the cutoff is treated using a reaction field method + with this dielectric constant. It should be set to 1 if another + implicit solvent model is being used (e.g., GB) + + Returns + ------- + NonbondedForce [, CustomNonbondedForce] + If a CustomNonbondedForce is necessary, the return value is a + 2-element tuple of NonbondedForce, CustomNonbondedForce. If only a + NonbondedForce is necessary, that is the return value + """ + if not self.atoms: return None + nonbfrc = super(AmberParm, self).omm_nonbonded_force( + nonbondedMethod, nonbondedCutoff, switchDistance, + ewaldErrorTolerance, reactionFieldDielectric + ) + has1012 = self.has_1012() + hasnbfix = self.has_NBFIX() + has1264 = 'LENNARD_JONES_CCOEF' in self.flag_list + if not hasnbfix and not has1264 and not has1012: + if self.chamber: + self._modify_nonb_exceptions(nonbfrc, None) + return nonbfrc + + # If we have NBFIX, omm_nonbonded_force returned a tuple + if hasnbfix: + nonbfrc = nonbfrc[0] + + # If we have a 10-12 potential, toggle hasnbfix since the 12-6 terms + # need to be handled via a lookup table (to avoid counting *both* 10-12 + # and 12-6 terms for the same pairs). Do this now, since nonbfrc is only + # a tuple if hasnbfix is True *without* 10-12 detection. + hasnbfix = hasnbfix or has1012 + + # We need a CustomNonbondedForce... determine what it needs to calculate + if hasnbfix and has1264: + if has1012: + force = mm.CustomNonbondedForce( + '(a/r6)^2-b/r6-c/r4+(ah/r6)^2-bh/(r6*r4); r6=r4*r2;' + 'r4=r2^2; r2=r^2;' + 'a=acoef(type1, type2);' + 'b=bcoef(type1, type2);' + 'c=ccoef(type1, type2);' + 'ah=ahcoef(type1, type2);' + 'bh=bhcoef(type1, type2);' + ) + else: + force = mm.CustomNonbondedForce( + '(a/r6)^2-b/r6-c/r4; r6=r4*r2;' + 'r4=r2^2; r2=r^2;' + 'a=acoef(type1, type2);' + 'b=bcoef(type1, type2);' + 'c=ccoef(type1, type2);' + ) + elif hasnbfix: + if has1012: + force = mm.CustomNonbondedForce( + '(a/r6)^2-b/r6+(ah/r6)^2-bh/(r6*r4);' + 'r6=r4*r2;r4=r2^2;r2=r^2;' + 'a=acoef(type1, type2);' + 'b=bcoef(type1, type2);' + 'ah=ahcoef(type1, type2);' + 'bh=bhcoef(type1, type2);' + ) + else: + force = mm.CustomNonbondedForce( + '(a/r6)^2-b/r6; r6=r2*r2*r2;' + 'r2=r^2; a=acoef(type1, type2);' + 'b=bcoef(type1, type2);' + ) + elif has1264: + force = mm.CustomNonbondedForce('-c/r^4;c=ccoef(type1, type2);') + + # Set up the force with all of the particles + force.addPerParticleParameter('type') + force.setForceGroup(self.NONBONDED_FORCE_GROUP) + for atom in self.atoms: + force.addParticle([atom.nb_idx-1]) + + # Now construct the lookup tables + ene_conv = u.kilocalories.conversion_factor_to(u.kilojoules) + length_conv = u.angstroms.conversion_factor_to(u.nanometers) + ntypes = self.parm_data['POINTERS'][PrmtopPointers.NTYPES] + if hasnbfix: + acoef = [0 for i in range(ntypes*ntypes)] + parm_acoef = self.parm_data['LENNARD_JONES_ACOEF'] + bcoef = acoef[:] + parm_bcoef = self.parm_data['LENNARD_JONES_BCOEF'] + if has1264: + ccoef = acoef[:] + parm_ccoef = self.parm_data['LENNARD_JONES_CCOEF'] + if has1012: + ahcoef = acoef[:] + bhcoef = acoef[:] + parm_ahcoef = self.parm_data['HBOND_ACOEF'] + parm_bhcoef = self.parm_data['HBOND_BCOEF'] + afac = sqrt(ene_conv) * length_conv**6 + bfac = ene_conv * length_conv**6 + cfac = ene_conv * length_conv**4 + ahfac = sqrt(ene_conv) * length_conv**6 + bhfac = ene_conv * length_conv**10 + nbidx = self.parm_data['NONBONDED_PARM_INDEX'] + for i in range(ntypes): + for j in range(ntypes): + idx = nbidx[ntypes*i+j] - 1 + ii = i + ntypes * j + if idx < 0 and has1012: + idx = -idx - 2 # Fix adjust for 0 since it was negative + ahcoef[ii] = sqrt(parm_ahcoef[idx]) * ahfac + bhcoef[ii] = parm_bhcoef[idx] * bhfac + if has1264: + ccoef[ii] = parm_ccoef[idx] * cfac + elif idx >= 0: + acoef[ii] = sqrt(parm_acoef[idx]) * afac + bcoef[ii] = parm_bcoef[idx] * bfac + if has1264: + ccoef[ii] = parm_ccoef[idx] * cfac + force.addTabulatedFunction('acoef', mm.Discrete2DFunction(ntypes, ntypes, acoef)) + force.addTabulatedFunction('bcoef', mm.Discrete2DFunction(ntypes, ntypes, bcoef)) + if has1264: + force.addTabulatedFunction('ccoef', mm.Discrete2DFunction(ntypes, ntypes, ccoef)) + # Our CustomNonbondedForce is taking care of the LJ part of our + # potential, so we need to go through nonbfrc and zero-out the + # Lennard-Jones parameters, but keep the charge parameters in place. + for i in range(nonbfrc.getNumParticles()): + chg, sig, eps = nonbfrc.getParticleParameters(i) + nonbfrc.setParticleParameters(i, chg, 0.5, 0.0) + # We still let the NonbondedForce handle our nonbonded exceptions, + # but we may have to modify the Lennard-Jones part with + # off-diagonal modifications. Offload this to a private method so it + # can be overridden in the ChamberParm class + self._modify_nonb_exceptions(nonbfrc, force) + elif has1264: + # Here we have JUST the r^-4 or r^-10/r^-12 parts, since the + # hasnbfix block above handled the "hasnbfix and has1264" case + if has1264: + ccoef = [0 for i in range(ntypes*ntypes)] + parm_ccoef = self.parm_data['LENNARD_JONES_CCOEF'] + cfac = ene_conv * length_conv**4 + ahfac = ene_conv * length_conv**12 + bhfac = ene_conv * length_conv**10 + nbidx = self.parm_data['NONBONDED_PARM_INDEX'] + for i in range(ntypes): + for j in range(ntypes): + idx = nbidx[ntypes*i+j] - 1 + if idx < 0: + continue + ccoef[i+ntypes*j] = parm_ccoef[idx] * cfac + force.addTabulatedFunction('ccoef', mm.Discrete2DFunction(ntypes, ntypes, ccoef)) + # Copy the exclusions + for ii in range(nonbfrc.getNumExceptions()): + i, j, qq, ss, ee = nonbfrc.getExceptionParameters(ii) + force.addExclusion(i, j) + if has1012: + force.addTabulatedFunction('ahcoef', mm.Discrete2DFunction(ntypes, ntypes, ahcoef)) + force.addTabulatedFunction('bhcoef', mm.Discrete2DFunction(ntypes, ntypes, bhcoef)) + # Copy the switching function information to the CustomNonbondedForce + if nonbfrc.getUseSwitchingFunction(): + force.setUseSwitchingFunction(True) + force.setSwitchingDistance(nonbfrc.getSwitchingDistance()) + # Set the dispersion correction on (by default) + force.setUseLongRangeCorrection(True) + # Determine which nonbonded method we should use and transfer the + # nonbonded cutoff + assert nonbondedMethod in {app.NoCutoff, app.CutoffNonPeriodic, app.PME, app.LJPME, app.Ewald, app.CutoffPeriodic}, 'Bad nonbondedMethod' + if nonbondedMethod is app.NoCutoff: + force.setNonbondedMethod(mm.CustomNonbondedForce.NoCutoff) + elif nonbondedMethod is app.CutoffNonPeriodic: + force.setNonbondedMethod(mm.CustomNonbondedForce.CutoffNonPeriodic) + elif nonbondedMethod in (app.PME, app.Ewald, app.CutoffPeriodic, app.LJPME): + force.setNonbondedMethod(mm.CustomNonbondedForce.CutoffPeriodic) + force.setCutoffDistance(nonbfrc.getCutoffDistance()) + + return nonbfrc, force + + #=================================================== + + # Iterators for parameters with and without hydrogen + @property + def bonds_inc_h(self): + """ All bonds including hydrogen """ + for bond in self.bonds: + if bond.atom1.atomic_number == 1 or bond.atom2.atomic_number == 1: + yield bond + + @property + def bonds_without_h(self): + """ All bonds without hydrogen """ + for bond in self.bonds: + if bond.atom1.atomic_number == 1 or bond.atom2.atomic_number == 1: + continue + yield bond + + @property + def angles_inc_h(self): + """ All angles including hydrogen """ + for angle in self.angles: + if (angle.atom1.atomic_number == 1 or angle.atom2.atomic_number == 1 + or angle.atom3.atomic_number == 1): + yield angle + + @property + def angles_without_h(self): + """ All angles including hydrogen """ + for angle in self.angles: + if (angle.atom1.atomic_number == 1 or angle.atom2.atomic_number == 1 + or angle.atom3.atomic_number == 1): + continue + yield angle + + @property + def dihedrals_inc_h(self): + """ All dihedrals including hydrogen """ + for dihed in self.dihedrals: + if (dihed.atom1.atomic_number == 1 + or dihed.atom2.atomic_number == 1 + or dihed.atom3.atomic_number == 1 + or dihed.atom4.atomic_number == 1): + yield dihed + + @property + def dihedrals_without_h(self): + """ All dihedrals including hydrogen """ + for dihed in self.dihedrals: + if (dihed.atom1.atomic_number == 1 + or dihed.atom2.atomic_number == 1 + or dihed.atom3.atomic_number == 1 + or dihed.atom4.atomic_number == 1): + continue + yield dihed + + #=================================================== + + @property + def chamber(self): + """ Whether this instance uses the CHARMM force field """ + return False + + @property + def amoeba(self): + """ Whether this instance uses the Amoeba force field """ + return False + + @property + def has_cmap(self): + """ Whether this instance has correction map terms or not """ + return len(self.cmaps) > 0 or (f'{self._cmap_prefix}CMAP_COUNT') in self.parm_data + + + #=========== PRIVATE INSTANCE METHODS ============ + + def _truncate_array(self, section, length): + """ Truncates an array to get the given length """ + self.parm_data[section] = self.parm_data[section][:length] + + #=================================================== + + def _load_cmap_info(self): + """ Loads the CHARMM CMAP types and array """ + if not self.has_cmap: return + del self.cmaps[:] + del self.cmap_types[:] + resolution_key = f"{self._cmap_prefix}CMAP_RESOLUTION" + parameter_key = f"{self._cmap_prefix}CMAP_PARAMETER_{{:02d}}" + for i in range(self.pointers['CMAP_TYPES']): + resolution = self.parm_data[resolution_key][i] + grid = self.parm_data[parameter_key.format(i + 1)] + cmts = self.parm_comments[parameter_key.format(i + 1)] + self.cmap_types.append(CmapType(resolution, grid, cmts, list=self.cmap_types)) + it = iter(self.parm_data[self._cmap_prefix + 'CMAP_INDEX']) + for i, j, k, l, m, n in zip(it, it, it, it, it, it): + self.cmaps.append( + Cmap(self.atoms[i-1], self.atoms[j-1], self.atoms[k-1], + self.atoms[l-1], self.atoms[m-1], self.cmap_types[n-1]) + ) + + #=================================================== + + def _check_section_lengths(self): + """ + Checks that all of the raw sections have the appropriate length as + specified by the POINTER section. + + Raises + ------ + AmberError if any of the lengths are incorrect + """ + def check_length(key, length, required=True): + if not required and key not in self.parm_data: + return + if len(self.parm_data[key]) != length: + raise AmberError(f'FLAG {key} has {len(self.parm_data[key])} elements; expected {length}') + natom = self.ptr('NATOM') + check_length('ATOM_NAME', natom) + check_length('CHARGE', natom) + check_length('MASS', natom) + check_length('ATOM_TYPE_INDEX', natom) + check_length('NUMBER_EXCLUDED_ATOMS', natom) + check_length('JOIN_ARRAY', natom) + check_length('IROTAT', natom) + check_length('RADIUS', natom, False) + check_length('SCREEN', natom, False) + check_length('ATOMIC_NUMBER', natom, False) + + ntypes = self.ptr('NTYPES') + check_length('NONBONDED_PARM_INDEX', ntypes*ntypes) + check_length('LENNARD_JONES_ACOEF', ntypes*(ntypes+1)//2) + check_length('LENNARD_JONES_BCOEF', ntypes*(ntypes+1)//2) + check_length('LENNARD_JONES_CCOEF', ntypes*(ntypes+1)//2, False) + + nres = self.ptr('NRES') + check_length('RESIDUE_LABEL', nres) + check_length('RESIDUE_POINTER', nres) + check_length('RESIDUE_CHAINID', nres, False) + check_length('RESIDUE_ICODE', nres, False) + check_length('RESIDUE_NUMBER', nres, False) + + check_length('BOND_FORCE_CONSTANT', self.ptr('NUMBND')) + check_length('BOND_EQUIL_VALUE', self.ptr('NUMBND')) + check_length('ANGLE_FORCE_CONSTANT', self.ptr('NUMANG')) + check_length('ANGLE_EQUIL_VALUE', self.ptr('NUMANG')) + check_length('DIHEDRAL_FORCE_CONSTANT', self.ptr('NPTRA')) + check_length('DIHEDRAL_PERIODICITY', self.ptr('NPTRA')) + check_length('DIHEDRAL_PHASE', self.ptr('NPTRA')) + check_length('SCEE_SCALE_FACTOR', self.ptr('NPTRA'), False) + check_length('SCNB_SCALE_FACTOR', self.ptr('NPTRA'), False) + check_length('SOLTY', self.ptr('NATYP')) + check_length('BONDS_INC_HYDROGEN', self.ptr('NBONH')*3) + check_length('BONDS_WITHOUT_HYDROGEN', self.ptr('MBONA')*3) + check_length('ANGLES_INC_HYDROGEN', self.ptr('NTHETH')*4) + check_length('ANGLES_WITHOUT_HYDROGEN', self.ptr('NTHETA')*4) + check_length('DIHEDRALS_INC_HYDROGEN', self.ptr('NPHIH')*5) + check_length('DIHEDRALS_WITHOUT_HYDROGEN', self.ptr('NPHIA')*5) + check_length('HBOND_ACOEF', self.ptr('NPHB')) + check_length('HBOND_BCOEF', self.ptr('NPHB')) + check_length('SOLVENT_POINTERS', 3, False) + if 'SOLVENT_POINTERS' in self.parm_data: + check_length('ATOMS_PER_MOLECULE', self.parm_data['SOLVENT_POINTERS'][1], False) + if self.has_cmap: + check_length(self._cmap_prefix + 'CMAP_COUNT', 2) + check_length(self._cmap_prefix + 'CMAP_RESOLUTION', self.pointers['CMAP_TYPES']) + resolution_key = self._cmap_prefix + 'CMAP_RESOLUTION' + parameter_key = self._cmap_prefix + 'CMAP_PARAMETER_%02d' + for i in range(self.pointers['CMAP_TYPES']): + res = self.parm_data[resolution_key][i] + check_length(parameter_key % (i+1), res*res) + + #=================================================== + + def _load_atoms_and_residues(self): + """ + Loads the atoms and residues (which are always done together) into the + data structure + """ + del self.residues[:] + del self.atoms[:] + # Figure out on which atoms the residues start and stop + natom = self.parm_data['POINTERS'][PrmtopPointers.NATOM] + res_ptr = self.parm_data['RESIDUE_POINTER'] + [natom+1] + try: + atnums = self.parm_data['ATOMIC_NUMBER'] + except KeyError: + atnums = None + try: + res_icd = self.parm_data['RESIDUE_ICODE'] + except KeyError: + res_icd = ['' for i in range(self.parm_data['POINTERS'][PrmtopPointers.NRES])] + try: + res_chn = self.parm_data['RESIDUE_CHAINID'] + except KeyError: + res_chn = ['' for i in range(self.parm_data['POINTERS'][PrmtopPointers.NRES])] + for i, resname in enumerate(self.parm_data['RESIDUE_LABEL']): + resstart = res_ptr[i] - 1 + resend = res_ptr[i+1] - 1 + for j in range(resstart, resend): + if atnums is None: + if self.parm_data['AMBER_ATOM_TYPE'][j] in ('EP', 'LP'): + atom = ExtraPoint() + else: + atom = Atom() + elif atnums[j] == 0: + atom = ExtraPoint() + else: + atom = Atom() + self.add_atom(atom, resname, i, res_chn[i], res_icd[i]) + # Residue number may not be unique, since zeros may be assigned to all + # solvent residues by addPDB. So we can't use the RESIDUE_NUMBER section + # to fill in the residue sequence IDs in the add_atom call above. Add it + # in as a post-hoc addition now if that information is present + if 'RESIDUE_NUMBER' in self.parm_data: + for res, num in zip(self.residues, self.parm_data['RESIDUE_NUMBER']): + res.number = num + + #=================================================== + + def _load_extra_exclusions(self): + """ + Look through the exclusion list in the prmtop file and see if any + _additional_ exclusions outside the basic ones defined for bonds, + angles, and dihedrals are specified. If so, add those to the exclusion + list. + + This also goes through all atoms and loads the proper bond, angle and + dihedral partners into any extra points. The way extra points are + handled is that they are considered to carry the same topological + connectivity as they actual atom they are bonded to. + """ + num_excluded = self.parm_data['NUMBER_EXCLUDED_ATOMS'] + excluded_list = self.parm_data['EXCLUDED_ATOMS_LIST'] + first_excl = 0 + for i, atom in enumerate(self.atoms): + exclusions = set() + bond_excl = set(atom._bond_partners + atom._angle_partners + + atom._dihedral_partners + atom._tortor_partners + + atom._exclusion_partners) + nexcl = num_excluded[i] + for j in range(first_excl, first_excl+nexcl): + if excluded_list[j]: # Zeros are placeholders + exclusions.add(self.atoms[excluded_list[j]-1]) + for eatom in exclusions - bond_excl: + atom.exclude(eatom) + first_excl += nexcl + + #=================================================== + + def _load_bond_info(self): + """ Loads the bond types and bond arrays """ + del self.bond_types[:] + del self.bonds[:] + for k, req in zip(self.parm_data['BOND_FORCE_CONSTANT'], self.parm_data['BOND_EQUIL_VALUE']): + self.bond_types.append(BondType(k, req, self.bond_types)) + it = iter(self.parm_data['BONDS_WITHOUT_HYDROGEN']) + for i, j, k in zip(it, it, it): + self.bonds.append( + Bond(self.atoms[i//3], self.atoms[j//3], self.bond_types[k-1]) + ) + it = iter(self.parm_data['BONDS_INC_HYDROGEN']) + for i, j, k in zip(it, it, it): + self.bonds.append( + Bond(self.atoms[i//3], self.atoms[j//3], self.bond_types[k-1]) + ) + + #=================================================== + + def _load_angle_info(self): + """ Loads the angle types and angle arrays """ + del self.angle_types[:] + del self.angles[:] + for k, theteq in zip( + self.parm_data['ANGLE_FORCE_CONSTANT'], self.parm_data['ANGLE_EQUIL_VALUE'] + ): + theteq *= RAD_TO_DEG + self.angle_types.append(AngleType(k, theteq, self.angle_types)) + it = iter(self.parm_data['ANGLES_WITHOUT_HYDROGEN']) + for i, j, k, l in zip(it, it, it, it): + self.angles.append( + Angle(self.atoms[i//3], self.atoms[j//3], self.atoms[k//3], self.angle_types[l-1]) + ) + it = iter(self.parm_data['ANGLES_INC_HYDROGEN']) + for i, j, k, l in zip(it, it, it, it): + self.angles.append( + Angle(self.atoms[i//3], self.atoms[j//3], self.atoms[k//3], self.angle_types[l-1]) + ) + + #=================================================== + + def _load_dihedral_info(self): + """ Loads the dihedral types and dihedral arrays """ + del self.dihedral_types[:] + del self.dihedrals[:] + try: + scee = self.parm_data['SCEE_SCALE_FACTOR'] + except KeyError: + scee = [1.2 for i in self.parm_data['DIHEDRAL_FORCE_CONSTANT']] + try: + scnb = self.parm_data['SCNB_SCALE_FACTOR'] + except KeyError: + scnb = [2.0 for i in self.parm_data['DIHEDRAL_FORCE_CONSTANT']] + for k, per, ph, e, n in zip( + self.parm_data['DIHEDRAL_FORCE_CONSTANT'], + self.parm_data['DIHEDRAL_PERIODICITY'], + self.parm_data['DIHEDRAL_PHASE'], + scee, + scnb, + ): + ph *= RAD_TO_DEG + self.dihedral_types.append( + DihedralType(k, per, ph, e, n, list=self.dihedral_types) + ) + it = iter(self.parm_data['DIHEDRALS_WITHOUT_HYDROGEN']) + for i, j, k, l, m in zip(it, it, it, it, it): + ignore_end = k < 0 + improper = l < 0 + self.dihedrals.append( + Dihedral(self.atoms[i//3], self.atoms[j//3], + self.atoms[abs(k)//3], self.atoms[abs(l)//3], + improper=improper, ignore_end=ignore_end, + type=self.dihedral_types[m-1]) + ) + it = iter(self.parm_data['DIHEDRALS_INC_HYDROGEN']) + for i, j, k, l, m in zip(it, it, it, it, it): + ignore_end = k < 0 + improper = l < 0 + self.dihedrals.append( + Dihedral( + self.atoms[i//3], self.atoms[j//3], + self.atoms[abs(k)//3], self.atoms[abs(l)//3], + improper=improper, ignore_end=ignore_end, + type=self.dihedral_types[m-1]) + ) + + #=================================================== + + def _xfer_atom_info(self): + """ + Sets the various topology file section data from the `atoms` list to the + topology file data in `parm_data` + """ + natom = len(self.atoms) + data = self.parm_data + data['POINTERS'][PrmtopPointers.NATOM] = natom + self.pointers['NATOM'] = natom + data['ATOM_NAME'] = [atom.name[:4] for atom in self.atoms] + data['AMBER_ATOM_TYPE'] = [atom.type[:4] for atom in self.atoms] + data['CHARGE'] = [atom.charge for atom in self.atoms] + data['MASS'] = [atom.mass for atom in self.atoms] + data['ATOM_TYPE_INDEX'] = [atom.nb_idx for atom in self.atoms] + data['JOIN_ARRAY'] = [atom.join for atom in self.atoms] + data['TREE_CHAIN_CLASSIFICATION'] = [atom.tree[:4] for atom in self.atoms] + data['IROTAT'] = [atom.irotat for atom in self.atoms] + data['NUMBER_EXCLUDED_ATOMS'] = [0 for atom in self.atoms] + if 'RADII' in data: + data['RADII'] = [atom.solvent_radius for atom in self.atoms] + if 'SCREEN' in data: + data['SCREEN'] = [atom.screen for atom in self.atoms] + if 'ATOMIC_NUMBER' in data: + data['ATOMIC_NUMBER'] = [atom.atomic_number for atom in self.atoms] + # Do the non-bonded exclusions now + data['EXCLUDED_ATOMS_LIST'] = [] + nextra = 0 + max_typ = 0 + for i, atom in enumerate(self.atoms): + excl = atom.nonbonded_exclusions(index_from=1) + if len(excl) == 0: + excl = [0] + data['EXCLUDED_ATOMS_LIST'] += excl + data['NUMBER_EXCLUDED_ATOMS'][i] = len(excl) + if atom.atomic_number == 0: + nextra += 1 + max_typ = max(max_typ, atom.nb_idx) + nnb = len(data['EXCLUDED_ATOMS_LIST']) + data['POINTERS'][PrmtopPointers.NNB] = nnb + self.pointers['NNB'] = self.pointers['NEXT'] = nnb + data['POINTERS'][PrmtopPointers.NUMEXTRA] = nextra + self.pointers['NUMEXTRA'] = nextra + max_typ = max(data['POINTERS'][PrmtopPointers.NTYPES], max_typ) + data['POINTERS'][PrmtopPointers.NTYPES] = max_typ + self.pointers['NTYPES'] = max_typ + + #=================================================== + + def _xfer_residue_info(self): + """ + Sets the various topology file section data from the `residues` list to + the topology file data in `parm_data` + """ + data = self.parm_data + nres = len(self.residues) + data['POINTERS'][PrmtopPointers.NRES] = nres + self.pointers['NRES'] = nres + data['RESIDUE_LABEL'] = [r.name[:4] for r in self.residues] + data['RESIDUE_POINTER'] = [r.atoms[0].idx+1 for r in self.residues] + if 'RESIDUE_NUMBER' in data: + data['RESIDUE_NUMBER'] = [r.number for r in self.residues] + if 'RESIDUE_CHAINID' in data: + data['RESIDUE_CHAINID'] = [res.chain for res in self.residues] + if 'RESIDUE_ICODE' in data: + data['RESIDUE_ICODE'] = [r.insertion_code for r in self.residues] + nmxrs = max([len(res) for res in self.residues]) if self.residues else 0 + data['POINTERS'][PrmtopPointers.NMXRS] = nmxrs + self.pointers['NMXRS'] = nmxrs + + #=================================================== + + def _xfer_bond_info(self): + """ + Sets the data for the various bond arrays in the raw data from the + parameter lists + """ + # First do the bond types + data = self.parm_data + for bond_type in self.bond_types: + bond_type.used = False + for bond in self.bonds: + bond.type.used = True + self.bond_types.prune_unused() + data['BOND_FORCE_CONSTANT'] = [type.k for type in self.bond_types] + data['BOND_EQUIL_VALUE'] = [type.req for type in self.bond_types] + data['POINTERS'][PrmtopPointers.NUMBND] = len(self.bond_types) + self.pointers['NUMBND'] = len(self.bond_types) + # Now do the bond arrays + data['BONDS_INC_HYDROGEN'] = bond_array = [] + bond_list = list(self.bonds_inc_h) + for bond in bond_list: + bond_array.extend([bond.atom1.idx*3, bond.atom2.idx*3, bond.type.idx+1]) + data['POINTERS'][PrmtopPointers.NBONH] = len(bond_list) + self.pointers['NBONH'] = len(bond_list) + data['BONDS_WITHOUT_HYDROGEN'] = bond_array = [] + bond_list = list(self.bonds_without_h) + for bond in bond_list: + bond_array.extend([bond.atom1.idx*3, bond.atom2.idx*3, bond.type.idx+1]) + data['POINTERS'][PrmtopPointers.MBONA] = len(bond_list) + data['POINTERS'][PrmtopPointers.NBONA] = len(bond_list) + self.pointers['MBONA'] = len(bond_list) + self.pointers['NBONA'] = len(bond_list) + + #=================================================== + + def _xfer_angle_info(self): + """ + Sets the data for the various angle arrays in the raw data from the + parameter lists + """ + # First do the angle types + data = self.parm_data + for angle_type in self.angle_types: + angle_type.used = False + for angle in self.angles: + angle.type.used = True + self.angle_types.prune_unused() + data['ANGLE_FORCE_CONSTANT'] = [type.k for type in self.angle_types] + data['ANGLE_EQUIL_VALUE'] = [type.theteq*DEG_TO_RAD for type in self.angle_types] + data['POINTERS'][PrmtopPointers.NUMANG] = len(self.angle_types) + self.pointers['NUMANG'] = len(self.angle_types) + # Now do the angle arrays + data['ANGLES_INC_HYDROGEN'] = angle_array = [] + angle_list = list(self.angles_inc_h) + for angle in angle_list: + angle_array.extend( + [angle.atom1.idx*3, angle.atom2.idx*3, angle.atom3.idx*3, angle.type.idx+1] + ) + data['POINTERS'][PrmtopPointers.NTHETH] = len(angle_list) + self.pointers['NTHETH'] = len(angle_list) + data['ANGLES_WITHOUT_HYDROGEN'] = angle_array = [] + angle_list = list(self.angles_without_h) + for angle in angle_list: + angle_array.extend( + [angle.atom1.idx*3, angle.atom2.idx*3, angle.atom3.idx*3, angle.type.idx+1] + ) + data['POINTERS'][PrmtopPointers.NTHETA] = len(angle_list) + data['POINTERS'][PrmtopPointers.MTHETA] = len(angle_list) + self.pointers['NTHETA'] = len(angle_list) + self.pointers['MTHETA'] = len(angle_list) + + #=================================================== + + def _xfer_dihedral_info(self): + """ + Sets the data for the various dihedral arrays in the raw data from the + parameter lists + """ + # First do the dihedral types + data = self.parm_data + for dihedral_type in self.dihedral_types: + dihedral_type.used = False + for dihed in self.dihedrals: + dihed.type.used = True + self.dihedral_types.prune_unused() + data['DIHEDRAL_FORCE_CONSTANT'] = [type.phi_k for type in self.dihedral_types] + data['DIHEDRAL_PERIODICITY'] = [type.per for type in self.dihedral_types] + data['DIHEDRAL_PHASE'] = [type.phase*DEG_TO_RAD for type in self.dihedral_types] + if 'SCEE_SCALE_FACTOR' in data: + data['SCEE_SCALE_FACTOR'] = [type.scee for type in self.dihedral_types] + if 'SCNB_SCALE_FACTOR' in data: + data['SCNB_SCALE_FACTOR'] = [type.scnb for type in self.dihedral_types] + data['POINTERS'][PrmtopPointers.NPTRA] = len(self.dihedral_types) + self.pointers['NPTRA'] = len(self.dihedral_types) + # Now do the dihedral arrays + data['DIHEDRALS_INC_HYDROGEN'] = dihed_array = [] + dihed_list = list(self.dihedrals_inc_h) + for dihed in dihed_list: + imp_sign = -1 if dihed.improper else 1 + end_sign = -1 if dihed.ignore_end else 1 + if dihed.atom3.idx == 0 or dihed.atom4.idx == 0: + dihed_array.extend( + [ + dihed.atom4.idx *3, + dihed.atom3.idx *3, + dihed.atom2.idx *3 * end_sign, + dihed.atom1.idx *3 * imp_sign, + dihed.type.idx + 1, + ] + ) + else: + dihed_array.extend( + [ + dihed.atom1.idx * 3, + dihed.atom2.idx * 3, + dihed.atom3.idx * 3 * end_sign, + dihed.atom4.idx * 3 * imp_sign, + dihed.type.idx + 1, + ] + ) + data['POINTERS'][PrmtopPointers.NPHIH] = len(dihed_list) + self.pointers['NPHIH'] = len(dihed_list) + data['DIHEDRALS_WITHOUT_HYDROGEN'] = dihed_array = [] + dihed_list = list(self.dihedrals_without_h) + for dihed in dihed_list: + imp_sign = -1 if dihed.improper else 1 + end_sign = -1 if dihed.ignore_end else 1 + if dihed.atom3.idx == 0 or dihed.atom4.idx == 0: + dihed_array.extend( + [ + dihed.atom4.idx *3, + dihed.atom3.idx *3, + dihed.atom2.idx *3 * end_sign, + dihed.atom1.idx *3 * imp_sign, + dihed.type.idx + 1, + ] + ) + else: + dihed_array.extend( + [ + dihed.atom1.idx * 3, + dihed.atom2.idx * 3, + dihed.atom3.idx * 3 * end_sign, + dihed.atom4.idx * 3 * imp_sign, + dihed.type.idx + 1, + ] + ) + data['POINTERS'][PrmtopPointers.NPHIA] = len(dihed_list) + data['POINTERS'][PrmtopPointers.MPHIA] = len(dihed_list) + self.pointers['NPHIA'] = len(dihed_list) + self.pointers['MPHIA'] = len(dihed_list) + + #=================================================== + + def _xfer_cmap_properties(self): + """ Sets the topology file section data from the cmap arrays """ + # If we have no cmaps, delete all remnants of the CMAP terms in the + # prmtop and bail out + if len(self.cmaps) == 0: + # We have deleted all cmaps. Get rid of them from the parm file and + # bail out. This is probably pretty unlikely, though... + flag_prefix = f"{self._cmap_prefix}CMAP" + flags_to_delete = [flag for flag in self.flag_list if flag.startswith(flag_prefix)] + for flag in flags_to_delete: + self.delete_flag(flag) + if 'CMAP' in self.pointers: + del self.pointers['CMAP'] + if 'CMAP_TYPES' in self.pointers: + del self.pointers['CMAP_TYPES'] + return + # Time to transfer our CMAP types + data = self.parm_data + for ct in self.cmap_types: + ct.used = False + for cmap in self.cmaps: + cmap.type.used = True + self.cmap_types.prune_unused() + # All of our CMAP types are in different topology file sections. We need + # to delete all of the CMAP_PARAMETER_XX sections and then + # recreate them with the correct size and comments. The comments have + # been stored in the CMAP types themselves to prevent them from being + # lost. We will also assume that the Fortran format we're going to use + # is the same for all CMAP types, so just pull it from + # CMAP_PARAMETER_01 (or fall back to 8(F9.5)) + parameter_key = self._cmap_prefix + 'CMAP_PARAMETER_%02d' + try: + fmt = str(self.formats[parameter_key % 1]) + except KeyError: + fmt = '8(F9.5)' + flags_to_delete = [] + for flag in self.flag_list: + if 'CMAP_PARAMETER' in flag: + flags_to_delete.append(flag) + for flag in flags_to_delete: + self.delete_flag(flag) + # Now add them back + after = self._cmap_prefix + 'CMAP_RESOLUTION' + for i, ct in enumerate(self.cmap_types): + newflag = self._cmap_prefix + 'CMAP_PARAMETER_%02d' % (i+1) + self.add_flag(newflag, fmt, data=ct.grid, comments=ct.comments, after=after) + after = newflag + # Now do the CMAP_INDEX section + data[self._cmap_prefix + 'CMAP_INDEX'] = cmap_array = [] + for cm in self.cmaps: + cmap_array.extend([cm.atom1.idx+1, cm.atom2.idx+1, cm.atom3.idx+1, cm.atom4.idx+1, + cm.atom5.idx+1, cm.type.idx+1]) + data[self._cmap_prefix + 'CMAP_COUNT'] = [len(self.cmaps), len(self.cmap_types)] + data[self._cmap_prefix + 'CMAP_RESOLUTION'] = [ct.resolution for ct in self.cmap_types] + self.pointers['CMAP'] = len(self.cmaps) + self.pointers['CMAP_TYPES'] = len(self.cmap_types) + + #=================================================== + + def _add_standard_flags(self): + """ Adds all of the standard flags to the parm_data array """ + self.set_version() + self.add_flag('TITLE', '20a4', num_items=0) + self.add_flag('POINTERS', '10I8', num_items=31) + self.add_flag('ATOM_NAME', '20a4', num_items=0) + self.add_flag('CHARGE', '5E16.8', num_items=0) + self.add_flag('ATOMIC_NUMBER', '10I8', num_items=0) + self.add_flag('MASS', '5E16.8', num_items=0) + self.add_flag('ATOM_TYPE_INDEX', '10I8', num_items=0) + self.add_flag('NUMBER_EXCLUDED_ATOMS', '10I8', num_items=0) + self.add_flag('NONBONDED_PARM_INDEX', '10I8', num_items=0) + self.add_flag('RESIDUE_LABEL', '20a4', num_items=0) + self.add_flag('RESIDUE_POINTER', '10I8', num_items=0) + self.add_flag('BOND_FORCE_CONSTANT', '5E16.8', num_items=0) + self.add_flag('BOND_EQUIL_VALUE', '5E16.8', num_items=0) + self.add_flag('ANGLE_FORCE_CONSTANT', '5E16.8', num_items=0) + self.add_flag('ANGLE_EQUIL_VALUE', '5E16.8', num_items=0) + self.add_flag('DIHEDRAL_FORCE_CONSTANT', '5E16.8', num_items=0) + self.add_flag('DIHEDRAL_PERIODICITY', '5E16.8', num_items=0) + self.add_flag('DIHEDRAL_PHASE', '5E16.8', num_items=0) + self.add_flag('SCEE_SCALE_FACTOR', '5E16.8', num_items=0) + self.add_flag('SCNB_SCALE_FACTOR', '5E16.8', num_items=0) + self.pointers['NATYP'] = 1 + self.parm_data['POINTERS'][PrmtopPointers.NATYP] = 1 + self.add_flag('SOLTY', '5E16.8', num_items=1) + self.add_flag('LENNARD_JONES_ACOEF', '5E16.8', num_items=0) + self.add_flag('LENNARD_JONES_BCOEF', '5E16.8', num_items=0) + self.add_flag('BONDS_INC_HYDROGEN', '10I8', num_items=0) + self.add_flag('BONDS_WITHOUT_HYDROGEN', '10I8', num_items=0) + self.add_flag('ANGLES_INC_HYDROGEN', '10I8', num_items=0) + self.add_flag('ANGLES_WITHOUT_HYDROGEN', '10I8', num_items=0) + self.add_flag('DIHEDRALS_INC_HYDROGEN', '10I8', num_items=0) + self.add_flag('DIHEDRALS_WITHOUT_HYDROGEN', '10I8', num_items=0) + self.add_flag('EXCLUDED_ATOMS_LIST', '10I8', num_items=0) + self.add_flag('HBOND_ACOEF', '5E16.8', num_items=0) + self.add_flag('HBOND_BCOEF', '5E16.8', num_items=0) + self.add_flag('HBCUT', '5E16.8', num_items=0) + self.add_flag('AMBER_ATOM_TYPE', '20a4', num_items=0) + self.add_flag('TREE_CHAIN_CLASSIFICATION', '20a4', num_items=0) + self.add_flag('JOIN_ARRAY', '10I8', num_items=0) + self.add_flag('IROTAT', '10I8', num_items=0) + if self.has_cmap: + self.add_flag(self._cmap_prefix + 'CMAP_COUNT', '2I8', num_items=2, + comments=['Number of CMAP terms, number of unique CMAP parameters']) + self.add_flag(self._cmap_prefix + 'CMAP_RESOLUTION', '20I4', num_items=0, + comments=['Number of steps along each phi/psi CMAP axis', + 'for each CMAP_PARAMETER grid']) + self.add_flag(self._cmap_prefix + 'CMAP_INDEX', '6I8', num_items=0, + comments=['Atom index i,j,k,l,m of the cross term', + 'and then pointer to CMAP_PARAMETER_n']) + if self.box is not None: + self.add_flag('SOLVENT_POINTERS', '3I8', num_items=3) + self.add_flag('ATOMS_PER_MOLECULE', '10I8', num_items=0) + self.add_flag('BOX_DIMENSIONS', '5E16.8', num_items=4) + self.add_flag('RADIUS_SET', '1a80', num_items=1) + self.add_flag('RADII', '5E16.8', num_items=0) + self.add_flag('SCREEN', '5E16.8', num_items=0) + self.add_flag('IPOL', '1I8', num_items=1) + + #=================================================== + + def _set_nonbonded_tables(self, nbfixes=None): + """ + Sets the tables of Lennard-Jones nonbonded interaction pairs + """ + ntypes = self.parm_data['POINTERS'][PrmtopPointers.NTYPES] + ntypes2 = ntypes * ntypes + # Set up the index lookup tables (not a unique solution) + self.parm_data['NONBONDED_PARM_INDEX'] = [0 for i in range(ntypes2)] + holder = [0 for i in range(ntypes2)] + idx = 0 + for i in range(ntypes): + for j in range(i+1): + idx += 1 + holder[ntypes*i+j] = holder[ntypes*j+i] = idx + idx = 0 + for i in range(ntypes): + for j in range(ntypes): + self.parm_data['NONBONDED_PARM_INDEX'][idx] = holder[ntypes*i+j] + idx += 1 + nttyp = ntypes * (ntypes + 1) // 2 + # Now build the Lennard-Jones arrays + self.parm_data['LENNARD_JONES_ACOEF'] = [0 for i in range(nttyp)] + self.parm_data['LENNARD_JONES_BCOEF'] = [0 for i in range(nttyp)] + self.recalculate_LJ() + # Now make any NBFIX modifications we had + if nbfixes is not None: + for i, fix in enumerate(nbfixes): + for terms in fix: + j, rmin, eps, rmin14, eps14 = terms + i, j = min(i, j-1), max(i, j-1) + eps = abs(eps) + eps14 = abs(eps14) + idx = self.parm_data['NONBONDED_PARM_INDEX'][ntypes*i+j] - 1 + self.parm_data['LENNARD_JONES_ACOEF'][idx] = eps * rmin**12 + self.parm_data['LENNARD_JONES_BCOEF'][idx] = 2*eps * rmin**6 + + #=================================================== + + @needs_openmm + def _modify_nonb_exceptions(self, nonbfrc, customforce): + """ + Modifies the nonbonded force exceptions and the custom nonbonded force + exclusions. The exceptions on the nonbonded force might need to be + adjusted if off-diagonal modifications on the L-J matrix are present + """ + # To get into this routine, either NBFIX is present OR this is a chamber + # prmtop and we need to pull the 1-4 L-J parameters from the + # LENNARD_JONES_14_A/BCOEF arrays + length_conv = u.angstroms.conversion_factor_to(u.nanometers) + ene_conv = u.kilocalories.conversion_factor_to(u.kilojoules) + atoms = self.atoms + try: + acoef = self.parm_data['LENNARD_JONES_14_ACOEF'] + bcoef = self.parm_data['LENNARD_JONES_14_BCOEF'] + except KeyError: + acoef = self.parm_data['LENNARD_JONES_ACOEF'] + bcoef = self.parm_data['LENNARD_JONES_BCOEF'] + nbidx = self.parm_data['NONBONDED_PARM_INDEX'] + ntypes = self.parm_data['POINTERS'][PrmtopPointers.NTYPES] + sigma_scale = 2**(-1/6) * length_conv + for ii in range(nonbfrc.getNumExceptions()): + i, j, qq, ss, ee = nonbfrc.getExceptionParameters(ii) + if qq.value_in_unit(u.elementary_charge**2) == 0 and ( + ss.value_in_unit(u.angstroms) == 0 or + ee.value_in_unit(u.kilocalories_per_mole) == 0): + # Copy this exclusion as-is... no need to modify the nonbfrc + # exception parameters + if customforce is not None: + customforce.addExclusion(i, j) + continue + # Figure out what the 1-4 scaling parameters were for this pair... + unscaled_ee = sqrt(self.atoms[i].epsilon_14 * self.atoms[j].epsilon_14) * ene_conv + try: + one_scnb = ee.value_in_unit(u.kilojoules_per_mole) / unscaled_ee + except ZeroDivisionError: + one_scnb = 1 + id1 = atoms[i].nb_idx - 1 + id2 = atoms[j].nb_idx - 1 + idx = nbidx[ntypes*id1+id2] - 1 + if idx >= 0: + a = acoef[idx] + b = bcoef[idx] + else: + a = b = 0 + if b == 0: + epsilon = 0.0 + sigma = 0.5 + elif idx >= 0: + # b / a == 2 / r^6 --> (a / b * 2)^(1/6) = rmin + rmin = (a / b * 2)**(1/6) + epsilon = b / (2 * rmin**6) * ene_conv * one_scnb + sigma = rmin * sigma_scale + nonbfrc.setExceptionParameters(ii, i, j, qq, sigma, epsilon) + if customforce is not None: + customforce.addExclusion(i, j) + + #=================================================== + + def _add_missing_13_14(self, ignore_inconsistent_vdw=False): + """ + Uses the bond graph to fill in zero-parameter angles and dihedrals. The + reason this is necessary is that Amber assumes that the list of angles + and dihedrals encompasses *all* 1-3 and 1-4 pairs as determined by the + bond graph, respectively. As a result, Amber programs use the angle and + dihedral lists to set nonbonded exclusions and exceptions. + + Parameters + ---------- + ignore_inconsistent_vdw : bool, optional + If True, do not make inconsistent 1-4 vdW parameters fatal. For + ChamberParm, the 1-4 specific vdW parameters can compensate. For + AmberParm, the 1-4 scaling factor cannot represent arbitrary + exceptions. Default is False (should only be True for ChamberParm) + + Returns + ------- + n13, n14 : int, int + The number of 1-3 and 1-4 pairs that needed to be added, + respectively. Purely diagnostic + """ + # We need to figure out what 1-4 scaling term to use if we don't have + # explicit exceptions + assert self.combining_rule in ('lorentz', 'geometric'), "Unrecognized combining rule" + if not self.adjusts: + scalings = defaultdict(int) + for dih in self.dihedrals: + if dih.ignore_end or dih.improper: continue + scalings[(dih.type.scee, dih.type.scnb)] += 1 + if len(scalings) > 0: + maxkey, maxval = next(iter(scalings.items())) + for key, val in scalings.items(): + if maxval < val: + maxkey, maxval = key, val + scee, scnb = maxkey + else: + scee = scnb = 1e10 + zero_torsion = DihedralType(0, 1, 0, scee, scnb) + else: + # Turn list of exceptions into a dict so we can look it up quickly + adjust_dict = dict() + for pair in self.adjusts: + adjust_dict[tuple(sorted([pair.atom1, pair.atom2]))] = pair + ignored_torsion = None + zero_torsion = None + # Scan through existing dihedrals to make sure the exceptions match + # the dihedral list + if self.combining_rule == 'lorentz': + comb_sig = lambda sig1, sig2: 0.5 * (sig1 + sig2) + elif self.combining_rule == 'geometric': + comb_sig = lambda sig1, sig2: sqrt(sig1 * sig2) + fac = 2**(1/6) + for dihedral in self.dihedrals: + if dihedral.ignore_end: continue + key = tuple(sorted([dihedral.atom1, dihedral.atom4])) + eref = sqrt(dihedral.atom1.epsilon_14 * dihedral.atom4.epsilon_14) + rref = comb_sig(dihedral.atom1.sigma_14, dihedral.atom4.sigma_14) * fac + if key in adjust_dict: + pair = adjust_dict[key] + if pair.type.epsilon == 0: + scnb = 1e10 + else: + scnb = eref / pair.type.epsilon + if pair.type.chgscale == 0: + scee = 1e10 + else: + scee = 1 / pair.type.chgscale + if ignore_inconsistent_vdw: + scnb = 1.0 + elif abs(rref - pair.type.rmin) > SMALL and pair.type.epsilon != 0: + raise TypeError('Cannot translate exceptions') + if (abs(scnb - dihedral.type.scnb) < SMALL and + abs(scee - dihedral.type.scee) < SMALL): + continue + else: + scee = scnb = 1e10 + newtype = _copy.copy(dihedral.type) + newtype.scee = scee + newtype.scnb = scnb + dihedral.type = newtype + newtype.list = self.dihedral_types + self.dihedral_types.append(newtype) + + zero_angle = AngleType(0, 0) + + n13 = n14 = 0 + if self.combining_rule == 'lorentz': + comb_sig = lambda sig1, sig2: 0.5 * (sig1 + sig2) + elif self.combining_rule == 'geometric': + comb_sig = lambda sig1, sig2: sqrt(sig1 * sig2) + fac = 2**(1/6) + for atom in self.atoms: + if isinstance(atom, ExtraPoint): continue + for batom in atom.bond_partners: + if isinstance(batom, ExtraPoint): continue + for aatom in batom.bond_partners: + if isinstance(aatom, ExtraPoint) or aatom is atom: continue + for datom in aatom.bond_partners: + if isinstance(datom, ExtraPoint): continue + if (datom in atom.angle_partners + atom.bond_partners + + atom.dihedral_partners or datom is atom): + continue + # Add the missing dihedral + if not self.adjusts: + tortype = zero_torsion + if n14 == 0: + tortype.list = self.dihedral_types + self.dihedral_types.append(tortype) + else: + # Figure out what the scale factors must be + key = tuple(sorted([atom, datom])) + if key not in adjust_dict: + if ignored_torsion is None: + ignored_torsion = DihedralType(0, 1, 0, 1e10, 1e10) + self.dihedral_types.append(ignored_torsion) + ignored_torsion.list = self.dihedral_types + tortype = ignored_torsion + elif 0 in (adjust_dict[key].type.epsilon, adjust_dict[key].type.rmin) \ + and adjust_dict[key].type.chgscale == 0: + if ignored_torsion is None: + ignored_torsion = DihedralType(0, 1, 0, 1e10, 1e10, + list=self.dihedral_types) + self.dihedral_types.append(ignored_torsion) + tortype = ignored_torsion + else: + pair = adjust_dict[key] + epsilon = pair.type.epsilon + rmin = pair.type.rmin + # Compare it to the 1-4 parameters that are + # already present + eref = sqrt(pair.atom1.epsilon_14 * pair.atom2.epsilon_14) + if pair.type.epsilon == 0: + scnb = 1e10 + else: + scnb = eref / epsilon + if pair.type.chgscale == 0: + scee = 1e10 + else: + scee = 1 / pair.type.chgscale + rref = comb_sig(pair.atom1.sigma_14, pair.atom2.sigma_14) * fac + if abs(rmin - rref) > SMALL: + if ignore_inconsistent_vdw: + scnb = 1.0 + else: + raise TypeError('Cannot translate exceptions') + tortype = DihedralType(0, 1, 0, scee, scnb, list=self.dihedral_types) + self.dihedral_types.append(tortype) + dihedral = Dihedral(atom, batom, aatom, datom, ignore_end=False, + improper=False, type=tortype) + self.dihedrals.append(dihedral) + n14 += 1 + if aatom in atom.angle_partners + atom.bond_partners: + continue + # Add the missing angle + self.angles.append(Angle(atom, batom, aatom, zero_angle)) + n13 += 1 + + if n13: + self.angle_types.append(zero_angle) + zero_angle.list = self.angle_types + if n14: # See if there is some ambiguity here + if not self.adjusts and len(scalings) > 1: + warn('Multiple 1-4 scaling factors detected. Using the most-used values scee=%f ' + 'scnb=%f' % (scee, scnb), AmberWarning) + return n13, n14 + + #=================================================== + + def _get_atom_collection_for_alternate_labels(self): + atom_collection = [defaultdict(list) for r in self.residues] + + for adict, residue in zip(atom_collection, self.residues): + for atom in residue.atoms: + adict[atom.name].append(atom) + return atom_collection + + def _label_alternates(self): + atom_collection = self._get_atom_collection_for_alternate_labels() + possible_labels = list('ABCDEFGHIJKLMNOPQRSTUVWXYZ') + + for _, adict in enumerate(atom_collection): + for atom_name, atom_list in adict.items(): + if len(atom_list) > 1: + for i, atom in enumerate(atom_list): + label = possible_labels[i%len(possible_labels)] + atom.altloc = label + + #=================================================== + + @property + def box(self): + if self._box is not None: + return self._box[0] + return None + + @box.setter + def box(self, value): + # Deleting the box is more complicated for AmberParm, so override it + # here + if value is None: + # Delete all of the other box info in the prmtop + self._box = None + for flag in ('IPTRES', 'NSPM', 'NSPSOL'): + if flag in self.pointers: + del self.pointers[flag] + for flag in ('SOLVENT_POINTERS', 'ATOMS_PER_MOLECULE', 'BOX_DIMENSIONS'): + self.delete_flag(flag) + self.hasbox = False + else: + if isinstance(value, np.ndarray): + box = value + else: + box = list(value) + if len(box) != 6: + raise ValueError('Box information must be 6 floats') + if u.is_quantity(box[0]): + box[0] = box[0].value_in_unit(u.angstroms) + if u.is_quantity(box[1]): + box[1] = box[1].value_in_unit(u.angstroms) + if u.is_quantity(box[2]): + box[2] = box[2].value_in_unit(u.angstroms) + if u.is_quantity(box[3]): + box[3] = box[3].value_in_unit(u.degrees) + if u.is_quantity(box[4]): + box[4] = box[4].value_in_unit(u.degrees) + if u.is_quantity(box[5]): + box[5] = box[5].value_in_unit(u.degrees) + box = np.array(box, dtype=np.float64, copy=False, subok=True).reshape((-1, 6)) + + # We are adding a box for the first time, so make sure we add some flags + if self._box is None: + self._box = box + # We need to add topology information + if 'SOLVENT_POINTERS' not in self.flag_list: + self.add_flag('SOLVENT_POINTERS', '3I8', num_items=3, after='IROTAT') + if 'ATOMS_PER_MOLECULE' not in self.flag_list: + self.add_flag('ATOMS_PER_MOLECULE', '10I8', data=[0], after='SOLVENT_POINTERS') + if 'BOX_DIMENSIONS' not in self.flag_list: + self.add_flag('BOX_DIMENSIONS', '5E16.8', after='ATOMS_PER_MOLECULE', + data=[box[0,3], box[0,0], box[0,1], box[0,2]]) + try: + self.rediscover_molecules(fix_broken=False) + except MoleculeError: + # Do not reorder molecules here -- only do that when + # specifically requested. Otherwise we could get out-of-sync + # with coordinates. + pass + self.load_pointers() + else: + self.parm_data['BOX_DIMENSIONS'] = [box[0,3], box[0,0], box[0,1], box[0,2]] + self._box = box + self._set_ifbox() + + def _set_ifbox(self): + """ Sets the IFBOX pointers to 1 (ortho), 2 (octahedral) , or 3 (other) """ + if self.box is None: + self.parm_data['POINTERS'][PrmtopPointers.IFBOX] = 0 + self.pointers['IFBOX'] = 0 + elif np.allclose(self.box[3:], 90): + self.parm_data['POINTERS'][PrmtopPointers.IFBOX] = 1 + self.pointers['IFBOX'] = 1 + elif np.allclose(self.box[3:], TRUNCATED_OCTAHEDRON_ANGLE, atol=0.02): + self.parm_data['POINTERS'][PrmtopPointers.IFBOX] = 2 + self.pointers['IFBOX'] = 2 + else: + # General triclinic + self.parm_data['POINTERS'][PrmtopPointers.IFBOX] = 3 + self.pointers['IFBOX'] = 3 + + def _cleanup_dihedrals_with_periodicity_zero(self): + """ + For torsions with only a single term and a periodicity set to 0, make sure pmemd still + properly recognizes the necessary exception parameters. update_dihedral_exclusions will + make sure that if a dihedral has a type pn0 *and* ignore_end is set to False (which means + that it is required to specify exclusions), then it is the *only* torsion between those + atoms in the system. This allows us to scan through our dihedrals, look for significant + terms that have pn==0, and simply add another dihedral with pn=1 and k=0 to ensure that + pmemd will always get that exception correct + """ + new_dihedrals = [] + for dih in self.dihedrals: + if dih.ignore_end or dih.type.per != 0: + continue + # If we got here, ignore_end must be False and out periodicity must be 0. So add + # another dihedral + dt = DihedralType(0, 1, 0, dih.type.scee, dih.type.scnb, list=self.dihedral_types) + self.dihedral_types.append(dt) + new_dihedrals.append( + Dihedral(dih.atom1, dih.atom2, dih.atom3, dih.atom4, improper=dih.improper, + ignore_end=False, type=dt) + ) + # Now that we added the above dihedral, we can start ignoring the end-group interactions + # on this dihedral + dih.ignore_end = True + if new_dihedrals: + self.dihedrals.extend(new_dihedrals) + + #=================================================== + + _AMBERPARM_ATTRS = 'LJ_types LJ_radius LJ_depth parm_data pointers'.split() + + def __getstate__(self): + d = Structure.__getstate__(self) + d.update(AmberFormat.__getstate__(self)) + for attr in self._AMBERPARM_ATTRS: + if getattr(self, attr, None) is not None: + d[attr] = getattr(self, attr) + return d + + def __setstate__(self, d): + AmberFormat.__setstate__(self, d) + Structure.__setstate__(self, d) + for attr in self._AMBERPARM_ATTRS: + if attr in d: + setattr(self, attr, d[attr]) + + +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +class Rst7(object): + """ + Amber input coordinate (or restart coordinate) file. Front-end for the + readers and writers, supports both NetCDF and ASCII restarts. + + Parameters + ---------- + filename : str, optional + If a filename is provided, this file is parsed and the Rst7 data + populated from that file. The format (ASCII or NetCDF) is autodetected + natom : int, optional + If no filename is provided, this value is required. If a filename is + provided, this value is ignored (and instead set to the value of natom + from the coordinate file). This is the number of atoms for which we have + coordinates. If not provided for a new file, it *must* be set later. + title : str, optional + For a file that is to be written, this is the title that will be given + to that file. Default is an empty string + time : float, optional + The time to write to the restart file. This is cosmetic. Default is 0 + """ + + def __init__(self, filename=None, natom=None, title='', time=0.0): + """ + Optionally takes a filename to read. This is deprecated, though, as the + alternative constructor "open" should be used instead + """ + self.coordinates = [] + self.vels = None + self._box = None + self.natom = natom + self.title = title + self.time = 0 + if filename is not None: + self.filename = filename + self._read(filename) + + @property + def box(self): + return self._box + @box.setter + def box(self, value): + if value is None: + self._box = None + else: + self._box = np.array(value).reshape((-1, 6))[0] + + @classmethod + def open(cls, filename): + """ Constructor that opens and parses an input coordinate file + + Parameters + ---------- + filename : str + Name of the file to parse + """ + inst = cls() + inst.filename = filename + inst._read(filename) + return inst + + def _read(self, filename): + """ + Open and parse an input coordinate file in either ASCII or NetCDF format + """ + try: + f = AmberAsciiRestart(filename, 'r') + self.natom = f.natom + except ValueError: + # Maybe it's a NetCDF file? + try: + f = NetCDFRestart.open_old(filename) + self.natom = f.atom + except (TypeError, RuntimeError): + raise AmberError('Could not parse restart file %s' % filename) + + self.coordinates = f.coordinates + if f.hasvels: + self.vels = f.velocities + if f.hasbox: + self.box = f.box + self.title = f.title + self.time = f.time + + @classmethod + def copy_from(cls, thing): + """ + Copies the coordinates, velocities, and box information from another + instance + """ + inst = cls() + inst.natom = thing.natom + inst.title = thing.title + inst.coordinates = thing.coordinates[:] + if hasattr(thing, 'vels'): inst.vels = _copy.deepcopy(thing.vels) + if hasattr(thing, 'box'): inst.box = _copy.deepcopy(thing.box) + inst.time = thing.time + + return inst + + def __copy__(self): + """ Copy constructor """ + return type(self).copy_from(self) + + def write(self, fname, netcdf=False): + """ Writes the coordinates and/or velocities to a restart file """ + if netcdf: + if self.natom is None: + raise RuntimeError('Number of atoms must be set for NetCDF ' + 'Restart files before write time') + f = NetCDFRestart.open_new(fname, self.natom, self.box is not None, + self.vels is not None, self.title) + else: + f = AmberAsciiRestart(fname, 'w', natom=self.natom, + title=self.title) + + f.time = self.time + # Now write the coordinates + f.coordinates = self.coordinates + if self.vels is not None: + f.velocities = self.vels + if self.box is not None: + f.box = self.box + f.close() + + @property + def positions(self): + """ Atomic coordinates with units """ + coordinates = self.coordinates.reshape(self.natom, 3) + return [Vec3(*x) for x in coordinates] * u.angstroms + + @property + def velocities(self): + """ Atomic velocities in units of angstroms/picoseconds """ + return np.array(self.vels, copy=False).reshape(self.natom, 3) + + @property + def box_vectors(self): + """ Unit cell vectors with units """ + if self.box is None: return None + return box_lengths_and_angles_to_vectors(*self.box) + + @property + def hasbox(self): + """ Whether or not this Rst7 has unit cell information """ + return self.box is not None + + @property + def hasvels(self): + """ Whether or not this Rst7 has velocities """ + return self.vels is not None + +def _zeros(length): + """ Returns an array of zeros of the given length """ + return [0 for i in range(length)] diff --git a/pmx/workflow/parmed/amber/_chamberparm.py b/pmx/workflow/parmed/amber/_chamberparm.py new file mode 100644 index 00000000..897c5850 --- /dev/null +++ b/pmx/workflow/parmed/amber/_chamberparm.py @@ -0,0 +1,680 @@ +""" +This module contains a chamber prmtop class that will read in all +parameters and allow users to manipulate that data and write a new +prmtop object. +""" +import copy as _copy +import warnings +from math import pi, sqrt + +from ..constants import DEG_TO_RAD, PrmtopPointers, RAD_TO_DEG, SMALL, TINY +from ..exceptions import AmberError, AmberWarning +from ..topologyobjects import BondType, ExtraPoint, Improper, ImproperType, UreyBradley +from ._amberparm import AmberParm + +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +class ChamberParm(AmberParm): + """Chamber Topology (parm7 format) class. + + Gives low, and some high, level access to topology data or + interact with some of the high-level classes comprising the system + topology and parameters. The ChamberParm class uses the same + attributes that the AmberParm class uses, and only the ones unique + to ChamberParm will be shown below. + + Parameters + ---------- + prm_name : str, optional + If provided, this file is parsed and the data structures will be loaded + from the data in this file + xyz : str or array, optional + If provided, the coordinates and unit cell dimensions from the provided + Amber inpcrd/restart file will be loaded into the molecule, or the + coordinates will be loaded from the coordinate array + box : array, optional + If provided, the unit cell information will be set from the provided + unit cell dimensions (a, b, c, alpha, beta, and gamma, respectively) + + Attributes + ---------- + LJ_14_radius : list(float) + The same as LJ_radius, except specific for 1-4 nonbonded parameters, + which may differ in the CHARMM force field + LJ_14_depth : list(float) + The same as LJ_depth, except specific for 1-4 nonbonded parameters, + which may differ in the CHARMM force field + urey_bradleys : TrackedList(UreyBradley) + List of Urey-Bradley terms between two atoms in a valence angle + impropers : TrackedList(Improper) + List of CHARMM-style improper torsions + cmaps : TrackedList(Cmap) + List of coupled-torsion correction map parameters + urey_bradley_types : TrackedList(UreyBradleyType) + List of parameters defining the Urey-Bradley terms + improper_types : TrackedList(Improper) + List of parameters defining the Improper terms + cmap_types : TrackedList(CmapType) + List of parameters defining the CMAP terms + chamber : bool=True + On ChamberParm instances, this is always True to indicate that it is a + CHAMBER-style topology file + amoeba : bool=False + On ChamberParm instances, this is always False to indicate that it is + not an AMOEBA-style topology file + has_cmap : bool + True if CMAP parameters are present in this system; False otherwise + + See Also + -------- + :class:`AmberParm` + + """ + + _cmap_prefix = "CHARMM_" + + #=================================================== + + def initialize_topology(self, xyz=None, box=None): + """ + Initializes topology data structures, like the list of atoms, bonds, + etc., after the topology file has been read. The following methods are + called: + """ + self.LJ_14_radius = [] + self.LJ_14_depth = [] + AmberParm.initialize_topology(self, xyz, box) + + #=================================================== + + def _copy_lj_data(self, other): + """ Copies Lennard-Jones lists and dicts from myself to a copy """ + super(ChamberParm, self)._copy_lj_data(other) + other.LJ_14_radius = _copy.copy(self.LJ_14_radius) + other.LJ_14_depth = _copy.copy(self.LJ_14_depth) + for atom in other.atoms: + other.LJ_14_radius[atom.nb_idx-1] = atom.atom_type.rmin_14 + other.LJ_14_depth[atom.nb_idx-1] = atom.atom_type.epsilon_14 + + #=================================================== + + def load_pointers(self): + """ + Loads the data in POINTERS section into a pointers dictionary with each + key being the pointer name according to http://ambermd.org/formats.html + """ + AmberParm.load_pointers(self) + # Other pointers + nub, nubtypes = self.parm_data['CHARMM_UREY_BRADLEY_COUNT'][:2] + self.pointers['NUB'] = nub + self.pointers['NUBTYPES'] = nubtypes + self.pointers['NIMPHI'] = self.parm_data['CHARMM_NUM_IMPROPERS'][0] + self.pointers['NIMPRTYPES'] = self.parm_data['CHARMM_NUM_IMPR_TYPES'][0] + + #=================================================== + + def load_structure(self): + """ + Loads all of the topology instance variables. This is necessary if we + actually want to modify the topological layout of our system + (like deleting atoms) + """ + super(ChamberParm, self).load_structure() + self._load_urey_brad_info() + self._load_improper_info() + # All of our angles are urey-bradley types + for angle in self.angles: angle.funct = 5 + super(ChamberParm, self).unchange() + + #=================================================== + + @classmethod + def from_structure(cls, struct, copy=False): + """ + Take a Structure instance and initialize a ChamberParm instance from + that data. + + Parameters + ---------- + struct : Structure + The input structure from which to construct a ChamberParm instance + copy : bool + If True, the input struct is deep-copied to make sure it does not + share any objects with the original ``struct``. Default is False + + Returns + ------- + inst : :class:`ChamberParm` + The ChamberParm instance derived from the input structure + + Notes + ----- + Due to the nature of the prmtop file, struct almost *always* returns a + deep copy. The one exception is when struct is already of type + :class:`ChamberParm`, in which case the original object is returned + unless ``copy`` is ``True``. + """ + if isinstance(struct, cls): + if copy: + return _copy.copy(struct) + return struct + if (struct.rb_torsions or struct.trigonal_angles or struct.pi_torsions + or struct.out_of_plane_bends or struct.stretch_bends + or struct.torsion_torsions or struct.multipole_frames): + raise TypeError('ChamberParm does not support all potential terms ' + 'defined in the input Structure') + inst = struct.copy(cls, split_dihedrals=True) + inst.update_dihedral_exclusions() + inst._add_missing_13_14(ignore_inconsistent_vdw=True) + inst.pointers = {} + inst.LJ_types = {} + nbfixes = inst.atoms.assign_nbidx_from_types() + # Give virtual sites a name that Amber understands + for atom in inst.atoms: + if isinstance(atom, ExtraPoint): atom.type = 'EP' + # Fill the Lennard-Jones arrays/dicts + ntyp = 0 + for atom in inst.atoms: + inst.LJ_types[atom.type] = atom.nb_idx + ntyp = max(ntyp, atom.nb_idx) + inst.LJ_radius = [0 for i in range(ntyp)] + inst.LJ_depth = [0 for i in range(ntyp)] + inst.LJ_14_radius = [0 for i in range(ntyp)] + inst.LJ_14_depth = [0 for i in range(ntyp)] + for atom in inst.atoms: + inst.LJ_radius[atom.nb_idx-1] = atom.rmin + inst.LJ_depth[atom.nb_idx-1] = atom.epsilon + inst.LJ_14_radius[atom.nb_idx-1] = atom.rmin_14 + inst.LJ_14_depth[atom.nb_idx-1] = atom.epsilon_14 + inst._add_standard_flags() + inst.pointers['NATOM'] = len(inst.atoms) + inst.parm_data['POINTERS'][PrmtopPointers.NATOM] = len(inst.atoms) + inst.box = _copy.copy(struct.box) + if struct.box is None: + inst.parm_data['POINTERS'][PrmtopPointers.IFBOX] = 0 + inst.pointers['IFBOX'] = 0 + elif (abs(struct.box[3] - 90) > TINY or abs(struct.box[4] - 90) > TINY + or abs(struct.box[5] - 90) > TINY): + inst.parm_data['POINTERS'][PrmtopPointers.IFBOX] = 2 + inst.pointers['IFBOX'] = 2 + inst.parm_data['BOX_DIMENSIONS'] = [struct.box[3]] + list(struct.box[:3]) + else: + inst.parm_data['POINTERS'][PrmtopPointers.IFBOX] = 1 + inst.pointers['IFBOX'] = 1 + inst.parm_data['BOX_DIMENSIONS'] = [90] + list(struct.box[:3]) + # pmemd likes to skip torsions with periodicities of 0, which may be + # present as a way to hack entries into the 1-4 pairlist. See + # https://github.com/ParmEd/ParmEd/pull/145 for discussion. The solution + # here is to simply set that periodicity to 1. + for dt in inst.dihedral_types: + if dt.phi_k == 0 and dt.per == 0: + dt.per = 1.0 + elif dt.per == 0: + warnings.warn('Periodicity of 0 detected with non-zero force constant. Changing ' + 'periodicity to 1 and force constant to 0 to ensure 1-4 nonbonded ' + 'pairs are properly identified. This might cause a shift in the ' + 'energy, but will leave forces unaffected', AmberWarning) + dt.phi_k = 0.0 + dt.per = 1.0 + inst.remake_parm() + inst._set_nonbonded_tables(nbfixes) + del inst.adjusts[:] + inst.parm_data['FORCE_FIELD_TYPE'] = fftype = [] + fftype.extend([1, 'CHARMM force field: No FF information parsed...']) + + return inst + + #=================================================== + + def remake_parm(self): + """ + Fills :attr:`parm_data` from the data in the parameter and topology + arrays (e.g., :attr:`atoms`, :attr:`bonds`, :attr:`bond_types`, ...) + """ + # Get rid of terms containing deleted atoms and empty residues + self.prune_empty_terms() + self.residues.prune() + self.rediscover_molecules() + + # Transfer information from the topology lists + self._xfer_atom_info() + self._xfer_residue_info() + self._xfer_bond_info() + self._xfer_angle_info() + self._xfer_dihedral_info() + self._xfer_urey_bradley_properties() + self._xfer_improper_properties() + self._xfer_cmap_properties() + # Load the pointers dict + self.load_pointers() + # Mark atom list as unchanged + super(ChamberParm, self).unchange() + + #=================================================== + + def fill_LJ(self): + """ + Fills the LJ_radius, LJ_depth arrays and LJ_types dictionary with data + from LENNARD_JONES_ACOEF and LENNARD_JONES_BCOEF sections of the prmtop + files, by undoing the canonical combining rules. + """ + AmberParm.fill_LJ(self) + + acoef = self.parm_data['LENNARD_JONES_14_ACOEF'] + bcoef = self.parm_data['LENNARD_JONES_14_BCOEF'] + ntypes = self.pointers['NTYPES'] + + self.LJ_14_radius = [] # empty LJ_radii so it can be re-filled + self.LJ_14_depth = [] # empty LJ_depths so it can be re-filled + one_sixth = 1.0 / 6.0 # we need to raise some numbers to the 1/6th power + + for i in range(ntypes): + lj_index = self.parm_data["NONBONDED_PARM_INDEX"][ntypes*i+i] - 1 + if acoef[lj_index] < 1.0e-6: + self.LJ_14_radius.append(0) + self.LJ_14_depth.append(0) + else: + factor = 2 * acoef[lj_index] / bcoef[lj_index] + self.LJ_14_radius.append(pow(factor, one_sixth) * 0.5) + self.LJ_14_depth.append(bcoef[lj_index] / 2 / factor) + + #=================================================== + + def recalculate_LJ(self): + """ + Takes the values of the LJ_radius and LJ_depth arrays and recalculates + the LENNARD_JONES_A/BCOEF topology sections from the canonical + combining rules. + """ + AmberParm.recalculate_LJ(self) + ntypes = self.pointers['NTYPES'] + acoef = self.parm_data['LENNARD_JONES_14_ACOEF'] + bcoef = self.parm_data['LENNARD_JONES_14_BCOEF'] + for i in range(ntypes): + for j in range(i, ntypes): + index = self.parm_data['NONBONDED_PARM_INDEX'][ntypes*i+j] - 1 + rij = self.LJ_14_radius[i] + self.LJ_14_radius[j] + wdij = sqrt(self.LJ_14_depth[i] * self.LJ_14_depth[j]) + acoef[index] = wdij * rij ** 12 + bcoef[index] = 2 * wdij * rij**6 + + #=================================================== + + @property + def chamber(self): + return True + + @property + def amoeba(self): + return False + + #=========== PRIVATE INSTANCE METHODS ============ + + def _load_urey_brad_info(self): + """ Loads the Urey-Bradley types and array """ + del self.urey_bradleys[:] + del self.urey_bradley_types[:] + for k, req in zip(self.parm_data['CHARMM_UREY_BRADLEY_FORCE_CONSTANT'], + self.parm_data['CHARMM_UREY_BRADLEY_EQUIL_VALUE']): + self.urey_bradley_types.append( + BondType(k, req, self.urey_bradley_types) + ) + it = iter(self.parm_data['CHARMM_UREY_BRADLEY']) + for i, j, k in zip(it, it, it): + self.urey_bradleys.append( + UreyBradley(self.atoms[i-1], self.atoms[j-1], + self.urey_bradley_types[k-1]) + ) + + #=================================================== + + def _load_improper_info(self): + """ Loads the CHARMM Improper types and array """ + del self.impropers[:] + del self.improper_types[:] + for k, eq in zip(self.parm_data['CHARMM_IMPROPER_FORCE_CONSTANT'], + self.parm_data['CHARMM_IMPROPER_PHASE']): + # Previous versions of ParmEd stored improper phases as degrees, + # whereas it should really be stored in radians. So do a simple + # heuristic check to see if a conversion is necessary so we support + # all versions. + eq = eq * RAD_TO_DEG if abs(eq) <= 2*pi else eq + self.improper_types.append( + ImproperType(k, eq, self.improper_types) + ) + it = iter(self.parm_data['CHARMM_IMPROPERS']) + for i, j, k, l, m in zip(it, it, it, it, it): + self.impropers.append( + Improper(self.atoms[i-1], self.atoms[j-1], self.atoms[k-1], + self.atoms[l-1], self.improper_types[m-1]) + ) + # Make sure that if we have a comment in the CHARMM impropers, we fix it + # to say the units are in radians + for i in range(len(self.parm_comments.get('CHARMM_IMPROPER_PHASE', []))): + comment = self.parm_comments['CHARMM_IMPROPER_PHASE'][i] + if 'degrees' in comment: + self.parm_comments['CHARMM_IMPROPER_PHASE'][i] = \ + comment.replace('degrees', 'radians') + + #=================================================== + + def _check_section_lengths(self): + """ Make sure each section has the necessary number of entries """ + super(ChamberParm, self)._check_section_lengths() + + def check_length(key, length, required=True): + if not required and not key in self.parm_data: return + if len(self.parm_data[key]) != length: + raise AmberError('FLAG %s has %d elements; expected %d' % + (key, len(self.parm_data[key]), length)) + + check_length('CHARMM_UREY_BRADLEY_COUNT', 2) + check_length('CHARMM_UREY_BRADLEY', self.pointers['NUB']*3) + check_length('CHARMM_UREY_BRADLEY_FORCE_CONSTANT', + self.pointers['NUBTYPES']) + check_length('CHARMM_UREY_BRADLEY_EQUIL_VALUE', + self.pointers['NUBTYPES']) + check_length('CHARMM_NUM_IMPROPERS', 1) + check_length('CHARMM_IMPROPERS', self.pointers['NIMPHI']*5) + check_length('CHARMM_NUM_IMPR_TYPES', 1) + check_length('CHARMM_IMPROPER_FORCE_CONSTANT', + self.pointers['NIMPRTYPES']) + check_length('CHARMM_IMPROPER_PHASE', self.pointers['NIMPRTYPES']) + + ntypes = self.pointers['NTYPES'] + check_length('LENNARD_JONES_14_ACOEF', ntypes*(ntypes+1)//2) + check_length('LENNARD_JONES_14_BCOEF', ntypes*(ntypes+1)//2) + + #=================================================== + + def _xfer_urey_bradley_properties(self): + """ + Sets the various topology file section data from the Urey-Bradley arrays + """ + data = self.parm_data + for urey_type in self.urey_bradley_types: + urey_type.used = False + for urey in self.urey_bradleys: + urey.type.used = True + self.urey_bradley_types.prune_unused() + data['CHARMM_UREY_BRADLEY_FORCE_CONSTANT'] = \ + [type.k for type in self.urey_bradley_types] + data['CHARMM_UREY_BRADLEY_EQUIL_VALUE'] = \ + [type.req for type in self.urey_bradley_types] + data['CHARMM_UREY_BRADLEY'] = bond_array = [] + for urey in self.urey_bradleys: + bond_array.extend([urey.atom1.idx+1, urey.atom2.idx+1, + urey.type.idx+1]) + nub = len(self.urey_bradleys) + nubt = len(self.urey_bradley_types) + data['CHARMM_UREY_BRADLEY_COUNT'] = [nub, nubt] + self.pointers['NUB'] = nub + self.pointers['NUBTYPES'] = nubt + + #=================================================== + + def _xfer_improper_properties(self): + """ Sets the topology file section data from the improper arrays """ + data = self.parm_data + for improper_type in self.improper_types: + improper_type.used = False + for improper in self.impropers: + improper.type.used = True + self.improper_types.prune_unused() + data['CHARMM_IMPROPER_FORCE_CONSTANT'] = \ + [type.psi_k for type in self.improper_types] + data['CHARMM_IMPROPER_PHASE'] = \ + [type.psi_eq*DEG_TO_RAD for type in self.improper_types] + data['CHARMM_IMPROPERS'] = improper_array = [] + for imp in self.impropers: + improper_array.extend([imp.atom1.idx+1, imp.atom2.idx+1, + imp.atom3.idx+1, imp.atom4.idx+1, + imp.type.idx+1]) + data['CHARMM_NUM_IMPROPERS'] = [len(self.impropers)] + data['CHARMM_NUM_IMPR_TYPES'] = [len(self.improper_types)] + self.pointers['NIMPHI'] = len(improper_array) + self.pointers['NIMPRTYPES'] = len(self.improper_types) + + #=================================================== + + def _add_standard_flags(self): + """ Adds all of the standard flags to the parm_data array """ + self.set_version() + self.add_flag('CTITLE', '20a4', num_items=0) + self.add_flag('POINTERS', '10I8', num_items=31) + self.add_flag('FORCE_FIELD_TYPE', 'i2,a78', num_items=0) + self.add_flag('ATOM_NAME', '20a4', num_items=0) + self.add_flag('CHARGE', '3E24.16', num_items=0, + comments=['Atomic charge multiplied by sqrt(332.0716D0) (CCELEC)']) + self.add_flag('ATOMIC_NUMBER', '10I8', num_items=0) + self.add_flag('MASS', '5E16.8', num_items=0) + self.add_flag('ATOM_TYPE_INDEX', '10I8', num_items=0) + self.add_flag('NUMBER_EXCLUDED_ATOMS', '10I8', num_items=0) + self.add_flag('NONBONDED_PARM_INDEX', '10I8', num_items=0) + self.add_flag('RESIDUE_LABEL', '20a4', num_items=0) + self.add_flag('RESIDUE_POINTER', '10I8', num_items=0) + self.add_flag('BOND_FORCE_CONSTANT', '5E16.8', num_items=0) + self.add_flag('BOND_EQUIL_VALUE', '5E16.8', num_items=0) + self.add_flag('ANGLE_FORCE_CONSTANT', '5E16.8', num_items=0) + self.add_flag('ANGLE_EQUIL_VALUE', '3E25.17', num_items=0) + self.add_flag('CHARMM_UREY_BRADLEY_COUNT', '2I8', num_items=2, + comments=['V(ub) = K_ub(r_ik - R_ub)**2', + 'Number of Urey Bradley terms and types']) + self.add_flag('CHARMM_UREY_BRADLEY', '10I8', num_items=0, + comments=['List of the two atoms and its parameter index', + 'in each UB term: i,k,index']) + self.add_flag('CHARMM_UREY_BRADLEY_FORCE_CONSTANT', '5E16.8', num_items=0, + comments=['K_ub: kcal/mol/A**2']) + self.add_flag('CHARMM_UREY_BRADLEY_EQUIL_VALUE', '5E16.8', num_items=0, + comments=['r_ub: A']) + self.add_flag('DIHEDRAL_FORCE_CONSTANT', '5E16.8', num_items=0) + self.add_flag('DIHEDRAL_PERIODICITY', '5E16.8', num_items=0) + self.add_flag('DIHEDRAL_PHASE', '5E16.8', num_items=0) + self.add_flag('SCEE_SCALE_FACTOR', '5E16.8', num_items=0) + self.add_flag('SCNB_SCALE_FACTOR', '5E16.8', num_items=0) + self.add_flag('CHARMM_NUM_IMPROPERS', '10I8', num_items=0, + comments=['Number of terms contributing to the', + 'quadratic four atom improper energy term:', + 'V(improper) = K_psi(psi - psi_0)**2']) + self.add_flag('CHARMM_IMPROPERS', '10I8', num_items=0, + comments=['List of the four atoms in each improper term', + 'i,j,k,l,index i,j,k,l,index', + 'where index is into the following two lists:', + 'CHARMM_IMPROPER_{FORCE_CONSTANT,IMPROPER_PHASE}']) + self.add_flag('CHARMM_NUM_IMPR_TYPES', '1I8', num_items=1, + comments=['Number of unique parameters contributing to the', + 'quadratic four atom improper energy term']) + self.add_flag('CHARMM_IMPROPER_FORCE_CONSTANT', '5E16.8', num_items=0, + comments=['K_psi: kcal/mole/rad**2']) + self.add_flag('CHARMM_IMPROPER_PHASE', '5E16.8', num_items=0, comments=['psi: radians']) + self.pointers['NATYP'] = 1 + self.parm_data['POINTERS'][PrmtopPointers.NATYP] = 1 + self.add_flag('SOLTY', '5E16.8', num_items=1) + self.add_flag('LENNARD_JONES_ACOEF', '3E24.16', num_items=0) + self.add_flag('LENNARD_JONES_BCOEF', '3E24.16', num_items=0) + self.add_flag('LENNARD_JONES_14_ACOEF', '3E24.16', num_items=0) + self.add_flag('LENNARD_JONES_14_BCOEF', '3E24.16', num_items=0) + self.add_flag('BONDS_INC_HYDROGEN', '10I8', num_items=0) + self.add_flag('BONDS_WITHOUT_HYDROGEN', '10I8', num_items=0) + self.add_flag('ANGLES_INC_HYDROGEN', '10I8', num_items=0) + self.add_flag('ANGLES_WITHOUT_HYDROGEN', '10I8', num_items=0) + self.add_flag('DIHEDRALS_INC_HYDROGEN', '10I8', num_items=0) + self.add_flag('DIHEDRALS_WITHOUT_HYDROGEN', '10I8', num_items=0) + self.add_flag('EXCLUDED_ATOMS_LIST', '10I8', num_items=0) + self.add_flag('HBOND_ACOEF', '5E16.8', num_items=0) + self.add_flag('HBOND_BCOEF', '5E16.8', num_items=0) + self.add_flag('HBCUT', '5E16.8', num_items=0) + self.add_flag('AMBER_ATOM_TYPE', '20a4', num_items=0) + self.add_flag('TREE_CHAIN_CLASSIFICATION', '20a4', num_items=0) + self.add_flag('JOIN_ARRAY', '10I8', num_items=0) + self.add_flag('IROTAT', '10I8', num_items=0) + if self.has_cmap: + self.add_flag(self._cmap_prefix + 'CMAP_COUNT', '2I8', num_items=2, + comments=['Number of CMAP terms, number of unique CMAP parameters']) + self.add_flag(self._cmap_prefix + 'CMAP_RESOLUTION', '20I4', num_items=0, + comments=['Number of steps along each phi/psi CMAP axis', + 'for each CMAP_PARAMETER grid']) + self.add_flag(self._cmap_prefix + 'CMAP_INDEX', '6I8', num_items=0, + comments=['Atom index i,j,k,l,m of the cross term', + 'and then pointer to CMAP_PARAMETER_n']) + if self.box is not None: + self.add_flag('SOLVENT_POINTERS', '3I8', num_items=3) + self.add_flag('ATOMS_PER_MOLECULE', '10I8', num_items=0) + self.add_flag('BOX_DIMENSIONS', '5E16.8', num_items=4) + self.add_flag('RADIUS_SET', '1a80', num_items=1) + self.add_flag('RADII', '5E16.8', num_items=0) + self.add_flag('SCREEN', '5E16.8', num_items=0) + self.add_flag('IPOL', '1I8', num_items=1) + + #=================================================== + + def _set_nonbonded_tables(self, nbfixes=None): + """ Sets the tables of Lennard-Jones nonbonded interaction pairs """ + from ..tools.actions import addLJType + data = self.parm_data + ntypes = data['POINTERS'][PrmtopPointers.NTYPES] + ntypes2 = ntypes * ntypes + # Set up the index lookup tables (not a unique solution) + data['NONBONDED_PARM_INDEX'] = [0 for i in range(ntypes2)] + holder = [0 for i in range(ntypes2)] + idx = 0 + for i in range(ntypes): + for j in range(i+1): + idx += 1 + holder[ntypes*i+j] = holder[ntypes*j+i] = idx + idx = 0 + for i in range(ntypes): + for j in range(ntypes): + data['NONBONDED_PARM_INDEX'][idx] = holder[ntypes*i+j] + idx += 1 + nttyp = ntypes * (ntypes + 1) // 2 + # Now build the Lennard-Jones arrays + data['LENNARD_JONES_14_ACOEF'] = [0 for i in range(nttyp)] + data['LENNARD_JONES_14_BCOEF'] = [0 for i in range(nttyp)] + data['LENNARD_JONES_ACOEF'] = [0 for i in range(nttyp)] + data['LENNARD_JONES_BCOEF'] = [0 for i in range(nttyp)] + self.recalculate_LJ() + # Now make any NBFIX modifications we had + if nbfixes is not None: + for i, fix in enumerate(nbfixes): + for terms in fix: + j, rmin, eps, rmin14, eps14 = terms + i, j = min(i, j-1), max(i, j-1) + eps = abs(eps) + eps14 = abs(eps14) + idx = data['NONBONDED_PARM_INDEX'][ntypes*i+j] - 1 + data['LENNARD_JONES_ACOEF'][idx] = eps * rmin**12 + data['LENNARD_JONES_BCOEF'][idx] = 2 * eps * rmin**6 + data['LENNARD_JONES_14_ACOEF'][idx] = eps14 * rmin14**12 + data['LENNARD_JONES_14_BCOEF'][idx] = 2 * eps14 * rmin14**6 + # If we had an explicit set of exceptions, we need to implement all of + # those exclusions. The electrostatic component of that exception is + # already handled (since a scaling factor *can* represent the full + # flexibility of electrostatic exceptions). The vdW component must be + # handled as 1-4 A- and B-coefficients. If vdW types are too compressed + # for this to be handled correctly, use "addLJType" to expand the types + # by 1 (so the tables only get as big as they *need* to get, to make + # sure they continue to fit in CUDA shared memory). + # + # The way we do this is to fill all of the elements with None, then fill + # them in as we walk through the 1-4 exceptions. If we hit a case where + # the target element is not None and *doesn't* equal the computed A- and + # B-coefficients, we have to expand our type list (assume all type + # names will have the same L-J parameters, which has been a fair + # assumption in my experience). + if not self.adjusts: return + for i in range(len(data['LENNARD_JONES_14_ACOEF'])): + data['LENNARD_JONES_14_ACOEF'][i] = None + data['LENNARD_JONES_14_BCOEF'][i] = None + ii = 0 + replaced_atoms = set() + while True: + needed_split = False + for pair in self.adjusts: + a1, a2 = pair.atom1, pair.atom2 + i, j = sorted([a1.nb_idx - 1, a2.nb_idx - 1]) + idx = data['NONBONDED_PARM_INDEX'][ntypes*i+j] - 1 + eps = pair.type.epsilon + rmin = pair.type.rmin + rmin6 = rmin * rmin * rmin * rmin * rmin * rmin + acoef = eps * rmin6*rmin6 + bcoef = 2 * eps * rmin6 + if data['LENNARD_JONES_14_ACOEF'][idx] is not None: + if abs(data['LENNARD_JONES_14_ACOEF'][idx] - acoef) > SMALL: + # Need to split out another type + needed_split = True + assert a1 not in replaced_atoms or a2 not in replaced_atoms + # Only add each atom as a new type ONCE + if a1 in replaced_atoms: + mask = '@%d' % (a2.idx+1) + replaced_atoms.add(a2) + else: + mask = '@%d' % (a1.idx+1) + replaced_atoms.add(a1) + addLJType(self, mask, radius_14=0, epsilon_14=0).execute() + ntypes += 1 + # None-out all of the added terms + j = ntypes - 1 + for i in range(j): + idx2 = data['NONBONDED_PARM_INDEX'][ntypes*i+j] - 1 + data['LENNARD_JONES_14_ACOEF'][idx2] = None + data['LENNARD_JONES_14_BCOEF'][idx2] = None + # We can stop here, since the next loop through the + # explicit exclusions will fill this in + else: + data['LENNARD_JONES_14_ACOEF'][idx] = acoef + data['LENNARD_JONES_14_BCOEF'][idx] = bcoef + ii += 1 + if not needed_split: + break + # The following should never happen + assert ii <= len(self.atoms)+1, 'Could not resolve all exceptions. This is a bug' + # Now go through and change all None's to 0s, as these terms won't be + # used for any exceptions, anyway + for i, item in enumerate(data['LENNARD_JONES_14_ACOEF']): + if item is None: + assert data['LENNARD_JONES_14_BCOEF'][i] is None, \ + 'A- and B- coefficients must be in lock-step!' + data['LENNARD_JONES_14_ACOEF'][i] = 0.0 + data['LENNARD_JONES_14_BCOEF'][i] = 0.0 + +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +def ConvertFromPSF(struct, params, title=''): + """ + This function instantiates a ChamberParm instance from a data structure + instantiated by a CHARMM PSF. + + Parameters + ---------- + struct : Structure + The structure object (typically loaded from a PSF file) + params : CharmmParameterSet + The parameter set describing the parameters of the input struct + title : str='' + The title to assign to the topology file + + Returns + ------- + ChamberParm + ChamberParm instance with all parameters loaded + """ + # Make sure all atom types are strings, not integers + int_starting = str(struct.atoms[0].atom_type) != struct.atoms[0].type + if int_starting: + for atom in struct.atoms: + atom.type = str(atom.atom_type) + parm = ChamberParm.from_structure(struct) + parm.parm_data['FORCE_FIELD_TYPE'] = fftype = [] + if params.parametersets == []: + params.parametersets.append('') + for pset in params.parametersets: + if 'CHARMM' not in pset: # needed to trigger "charmm_active"... + pset = 'CHARMM: %s' % pset + fftype.extend([len(params.parametersets), pset]) + + # Convert atom types back to integers if that's how they started + if int_starting: + for atom in struct.atoms: + atom.type = int(atom.atom_type) + return parm diff --git a/pmx/workflow/parmed/amber/_tinkerparm.py b/pmx/workflow/parmed/amber/_tinkerparm.py new file mode 100644 index 00000000..7b21ad85 --- /dev/null +++ b/pmx/workflow/parmed/amber/_tinkerparm.py @@ -0,0 +1,1167 @@ +""" +This module contains an amber prmtop class that will read in all +parameters and allow users to manipulate that data and write a new +prmtop object. +""" +import numpy as np + +from ..constants import DEG_TO_RAD, PrmtopPointers, RAD_TO_DEG +from ..exceptions import AmberError +from ..formats.registry import load_file +from ..topologyobjects import AmoebaNonbondedExceptionType as NonbondedExceptionType +from ..topologyobjects import (Angle, AngleType, Bond, BondType, ChiralFrame, + Dihedral, DihedralType, MultipoleFrame, + NonbondedException, OutOfPlaneBend, + OutOfPlaneBendType, PiTorsion, StretchBend, + StretchBendType, TorsionTorsion, + TorsionTorsionType, TrigonalAngle, UreyBradley) +from ._amberparm import AmberParm +from .amberformat import AmberFormat + + +class AmoebaParm(AmberParm): + """ + Tinker Topology (parm7 format) class defining the AMOEBA force field. Gives + low, and some high, level access to topology data. You can interact with + the raw data in the topology file directly or interact with some of the + high-level classes comprising the system topology and parameters. + + Parameters + ---------- + prm_name : str=None + If provided, this file is parsed and the data structures will be loaded + from the data in this file + rst7_name : str=None + If provided, the coordinates and unit cell dimensions from the provided + Amber inpcrd/restart file will be loaded into the molecule + + Attributes + ---------- + parm_data : dict {str : list} + A dictionary that maps FLAG names to all of the data contained in that + section of the Amber file. + formats : dict {str : FortranFormat} + A dictionary that maps FLAG names to the FortranFormat instance in which + the data is stored in that section + parm_comments : dict {str : list} + A dictionary that maps FLAG names to the list of COMMENT lines that were + stored in the original file + flag_list : list + An ordered list of all FLAG names. This must be kept synchronized with + `parm_data`, `formats`, and `parm_comments` such that every item in + `flag_list` is a key to those 3 dicts and no other keys exist + version : str + The VERSION string from the Amber file + name : str + The file name of the originally parsed file (set to the fname parameter) + atoms : AtomList(Atom) + List of all atoms in the system + residues : ResidueList(Residue) + List of all residues in the system + bonds : TrackedList(Bond) + List of bonds between two atoms in the system + angles : TrackedList(Angle) + List of regular angles between three atoms in the system + dihedrals : TrackedList(Angle) + List of all proper torsions between 4 atoms in the system + urey_bradleys : TrackedList(UreyBradley) + List of all Urey-Bradley terms between 2 atoms connected by an angle + trigonal_angles : TrackedList(TrigonalAngle) + List of all trigonal angle terms + out_of_plane_bends : TrackedList(OutOfPlaneBend) + List of all out-of-plane bending terms + pi_torsions : TrackedList(PiTorsion) + List of all pi-torsion terms + stretch_bends : TrackedList(StretchBend) + List of all stretch-bending terms + torsion_torsions : TrackedList(TorsionTorsion) + List of all coupled torsion-torsion terms + chiral_frames : TrackedList(ChiralFrame) + List of all chiral centers + multipole_frames : TrackedList(MultipoleFrame) + List of all multipole frames of reference + adjusts : TrackedList(NonbondedException) + List of all nonbonded exception parameters used for adjusting nonbonded + interactions between particular pairs of atoms + box : list of 6 floats + Periodic boundary unit cell dimensions and angles + bond_types : TrackedList(BondType) + The bond types containing the parameters for each bond stretching term + angle_types : TrackedList(AngleType) + The angle types containing the parameters for each angle bending term + dihedral_types : TrackedList(DihedralType) + The dihedral types containing the parameters for each torsional term + urey_bradley_types : TrackedList(BondType) + The Urey-Bradley types containing the parameters for each term + trigonal_angle_types : TrackedList(AngleType) + The trigonal angle types containing the parameters for each term + out_of_plane_bend_types : TrackedList(AngleType) + The out-of-plane bending angle type containing parameters for each term + pi_torsion_types : TrackedList(DihedralType) + The pi-torsion type containing parameters for each torsional term + stretch_bend_types : TrackedList(StretchBendType) + The stretch-bend type containing parameters for each term + torsion_torsion_types : TrackedList(TorsionTorsionType) + The coupled torsion-torsion type containing parameters for coupled + torsions + adjust_types : TrackedList(NonbondedExceptionType) + The nonbonded exception scaling factors for pairs of particles + bonds_inc_h : iterator(Bond) + Read-only generator that loops through all bonds that contain Hydrogen + bonds_without_h : iterator(Bond) + Read-only generator that loops through all bonds that do not contain + Hydrogen + angles_inc_h : iterator(Angle) + Read-only generator that loops through all angles that contain Hydrogen + angles_without_h : iterator(Angle) + Read-only generator that loops through all angles that do not contain + Hydrogen + dihedrals_inc_h : iterator(Dihedral) + Read-only generator that loops through all dihedrals that contain + Hydrogen + dihedrals_without_h : iterator(Dihedral) + Read-only generator that loops through all dihedrals that do not contain + Hydrogen + chamber : bool=False + On AmberParm instances, this is always False to indicate that it is not + a CHAMBER-style topology file + amoeba : bool=False + On AmberParm instances, this is always False to indicate that it is not + an AMOEBA-style topology file + has_cmap : bool=False + On AmberParm instances, this is always False to indicate that it does + not have correction maps (unique to CHARMM force field and chamber + topologies) + """ + + #============================================= + + def initialize_topology(self, xyz=None, box=None): + """ + Initializes topology data structures, like the list of atoms, bonds, + etc., after the topology file has been read. + + Raises + ------ + AmberError if it is not an Amoeba-styled topology file + """ + try: + if self.parm_data['AMOEBA_FORCEFIELD'][0] != 1: + raise AmberError('Bad AMOEBA-format topology') + except KeyError: + raise AmberError('Bad AMOEBA-format topology') + + # We need to handle RESIDUE_ICODE properly since it may have picked up + # some extra values + if 'RESIDUE_ICODE' in self.parm_data: + self._truncate_array('RESIDUE_ICODE', self.parm_data['POINTERS'][PrmtopPointers.NRES]) + + self.LJ_types = {} + self.LJ_radius = [] + self.LJ_depth = [] + # If we were given a prmtop, read it in + self.pointers = {} + self.load_pointers() + + # Load the structure arrays + self.load_structure() + + if isinstance(xyz, str): + f = load_file(xyz) + if not hasattr(f, 'coordinates') or f.coordinates is None: + raise TypeError('%s does not have coordinates' % xyz) + self.coordinates = f.coordinates + if hasattr(f, 'box') and f.box is not None and box is None: + self.box = f.box + else: + self.coordinates = xyz + if box is not None: + self.box = box + + # If all else fails, set the box from the prmtop file + if self.parm_data['POINTERS'][PrmtopPointers.IFBOX] > 0 and self.box is None: + box = self.parm_data['BOX_DIMENSIONS'] + self.box = list(box[1:]) + [box[0], box[0], box[0]] + + self.hasbox = self.box is not None + + #============================================= + + def load_structure(self): + """ + Loads all of the topology instance variables. This is necessary if we + actually want to modify the topological layout of our system + """ + self._load_atoms_and_residues() + self.load_atom_info() + self._load_bond_info() + self._load_angle_info() + self._load_urey_bradley_info() + self._load_trigonal_angle_info() + self._load_oopbend_info() + self._load_dihedral_info() + self._load_pitorsion_info() + self._load_stretch_bend_info() + self._load_torsion_torsion_info() + self._load_frame_info() + self._load_exception_info() + super(AmoebaParm, self).unchange() + + #============================================= + + def load_atom_info(self): + """ + Loads atom properties into the atoms that have been loaded. If any + arrays are too short or too long, an IndexError will be raised + """ + anam = self.parm_data['ATOM_NAME'] + mass = self.parm_data['MASS'] + atyp = self.parm_data['AMBER_ATOM_TYPE'] + tree = self.parm_data['TREE_CHAIN_CLASSIFICATION'] + join = self.parm_data['JOIN_ARRAY'] + irot = self.parm_data['IROTAT'] + typi = self.parm_data['AMOEBA_ATOM_TYPE_INDEX'] + atnum = self.parm_data['AMOEBA_ATOMIC_NUMBER'] + clsi = self.parm_data['AMOEBA_ATOM_CLASS_INDEX'] + nbtyp = self.parm_data['AMOEBA_VDW_ATOM_TYPES_LIST'] + vdwp = self.parm_data['AMOEBA_VDW_ATOM_PARENT_LIST'] + vdww = self.parm_data['AMOEBA_VDW_PARENT_COORD_WEIGHT_LIST'] + mpole = self.parm_data['AMOEBA_LOCAL_FRAME_MULTIPOLES_LIST'] + pol = self.parm_data['AMOEBA_POLARIZABILITY_LIST'] + for i, atom in enumerate(self.atoms): + i10 = i * 10 + multipoles = mpole[i10:i10+10] + atom.name = anam[i] + atom.type = atyp[i] + atom.mass = mass[i] + atom.tree = tree[i] + atom.join = join[i] + atom.irotat = irot[i] + atom.type_idx = typi[i] + atom.atomic_number = atnum[i] + atom.class_idx = clsi[i] + atom.vdw_parent = self.atoms[vdwp[i]-1] + atom.vdw_weight = vdww[i] + atom.charge = multipoles[0] # monopole is charge + atom.multipoles = multipoles + atom.polarizability = pol[i] + atom.nb_idx = nbtyp[i] + + #============================================= + + def remake_parm(self): + """ Recomputes the topology file parameters and fills parm_data """ + # Get rid of terms containing deleted atoms and empty residues + self.prune_empty_terms() + self.residues.prune() + self.rediscover_molecules() + + # Transfer information from the topology lists + self._xfer_atom_info() + self._xfer_residue_info() + self._xfer_bond_info() + self._xfer_angle_info() + self._xfer_urey_bradley_info() + self._xfer_trigonal_angle_info() + self._xfer_oopbend_info() + self._xfer_dihedral_info() + self._xfer_pitorsion_info() + self._xfer_stretch_bend_info() + self._xfer_torsion_torsion_info() + self._xfer_frame_info() + self._xfer_exception_info() + + #============================================= + + def mdin_skeleton(self): + """ + Returns the skeleton of an mdin file with the &amoeba namelist set up + correctly for the potential terms that are present in this topology + file. + + Returns + ------- + str + A skeleton MDIN file with all of the do_* variables in the &amoeba + section set correctly. It is commented for easy editing + """ + return ('Input file for AMOEBA simulations.\n' + ' &cntrl\n' + ' ! Add whatever variables you need here\n' + ' ntb=1, ntt=1, ntp=0, ! PBC, thermostat, barostat\n' + ' irest=0, ntx=1, ! restart flags\n' + ' /\n' + ' &amoeba\n' + ' ! Some basic potential parameters. For better\n' + ' ! energy conservation you need to adjust these\n' + ' ! defaults\n' + ' beeman_integrator=1, ! Use Beeman integrator\n' + ' dipole_scf_tol=0.01, ! 10e-6 gives good NVE\n' + '\n' + ' ! You should not generally modify these variables:\n' + ' do_valence=1, do_bond=%d, do_ureyb=%d,\n' + ' do_reg_angle=%d, do_trig_angle=%d, do_opbend=%d,\n' + ' do_torsion=%d, do_pi_torsion=%d, do_strbend=%d,\n' + ' do_torsion_torsion=%d,\n' + ' /\n' % (bool(self.bonds), bool(self.urey_bradleys), + bool(self.angles), bool(self.trigonal_angles), + bool(self.out_of_plane_bends), bool(self.dihedrals), + bool(self.pi_torsions), bool(self.stretch_bends), + bool(self.torsion_torsions)) + ) + + #============================================= + + @property + def chamber(self): + return False + + @property + def amoeba(self): + return True + + #=========== PRIVATE INSTANCE METHODS ============ + + def _load_bond_info(self): + """ Load the regular AMOEBA bonds, if they exist """ + if not 'AMOEBA_REGULAR_BOND_LIST' in self.parm_data: return + data = self.parm_data + del self.bonds[:] + del self.bond_types[:] + for k, req in zip(data['AMOEBA_REGULAR_BOND_FORCE_CONSTANT'], + data['AMOEBA_REGULAR_BOND_EQUIL_VALUE']): + self.bond_types.append(BondType(k, req, self.bond_types)) + self.bond_types.degree = data['AMOEBA_REGULAR_BOND_FTAB_DEGREE'][0] + self.bond_types.coeffs = data['AMOEBA_REGULAR_BOND_FTAB_COEFFS'][:] + if len(self.bond_types.coeffs) != self.bond_types.degree + 1: + raise AmberError('Bond degree (%d) does not make sense with %d ' + 'coefficients' % (self.bond_types.degree, + len(self.bond_types.coeffs))) + it = iter(self.parm_data['AMOEBA_REGULAR_BOND_LIST']) + for i, j, k in zip(it, it, it): + self.bonds.append( + Bond(self.atoms[i-1], self.atoms[j-1], + self.bond_types[k-1]) + ) + + #============================================= + + def _load_angle_info(self): + """ Load the regular AMOEBA angles, if they exist """ + if not 'AMOEBA_REGULAR_ANGLE_LIST' in self.parm_data: return + data = self.parm_data + del self.angles[:] + del self.angle_types[:] + for k, eq in zip(data['AMOEBA_REGULAR_ANGLE_FORCE_CONSTANT'], + data['AMOEBA_REGULAR_ANGLE_EQUIL_VALUE']): + self.angle_types.append(AngleType(k, eq, self.angle_types)) + self.angle_types.degree = data['AMOEBA_REGULAR_ANGLE_FTAB_DEGREE'][0] + self.angle_types.coeffs = data['AMOEBA_REGULAR_ANGLE_FTAB_COEFFS'][:] + if len(self.angle_types.coeffs) != self.angle_types.degree + 1: + raise AmberError('Angle degree (%d) does not make sense with %d ' + 'coefficients' % (self.angle_types.degree, + len(self.angle_types.coeffs))) + it = iter(data['AMOEBA_REGULAR_ANGLE_LIST']) + for i, j, k, l in zip(it, it, it, it): + self.angles.append( + Angle(self.atoms[i-1], self.atoms[j-1], self.atoms[k-1], + self.angle_types[l-1]) + ) + + #============================================= + + def _load_urey_bradley_info(self): + """ Loads the AMOEBA Urey-Bradley terms, if they exist """ + if not 'AMOEBA_UREY_BRADLEY_BOND_LIST' in self.parm_data: return + data = self.parm_data + del self.urey_bradleys[:] + del self.urey_bradley_types[:] + for k, eq in zip(data['AMOEBA_UREY_BRADLEY_BOND_FORCE_CONSTANT'], + data['AMOEBA_UREY_BRADLEY_BOND_EQUIL_VALUE']): + self.urey_bradley_types.append( + BondType(k, eq, self.urey_bradley_types) + ) + self.urey_bradley_types.degree = degree = \ + data['AMOEBA_UREY_BRADLEY_BOND_FTAB_DEGREE'][0] + self.urey_bradley_types.coeffs = coeffs = \ + data['AMOEBA_UREY_BRADLEY_BOND_FTAB_COEFFS'][:] + if len(coeffs) != degree + 1: + raise AmberError('Urey-Bradley degree (%d) does not make sense ' + 'with %d coefficients' % (degree, len(coeffs))) + it = iter(data['AMOEBA_UREY_BRADLEY_BOND_LIST']) + for i, j, k in zip(it, it, it): + self.urey_bradleys.append( + UreyBradley(self.atoms[i-1], self.atoms[j-1], + self.urey_bradley_types[k-1]) + ) + + #============================================= + + def _load_trigonal_angle_info(self): + """ Loads the AMOEBA trigonal angle terms, if they exist """ + if not 'AMOEBA_TRIGONAL_ANGLE_LIST' in self.parm_data: return + data = self.parm_data + del self.trigonal_angles[:] + del self.trigonal_angle_types[:] + for k, eq in zip(data['AMOEBA_TRIGONAL_ANGLE_FORCE_CONSTANT'], + data['AMOEBA_TRIGONAL_ANGLE_EQUIL_VALUE']): + self.trigonal_angle_types.append( + AngleType(k, eq, self.trigonal_angle_types) + ) + self.trigonal_angle_types.degree = degree = \ + data['AMOEBA_TRIGONAL_ANGLE_FTAB_DEGREE'][0] + self.trigonal_angle_types.coeffs = coeffs = \ + data['AMOEBA_TRIGONAL_ANGLE_FTAB_COEFFS'][:] + if len(coeffs) != degree + 1: + raise AmberError('Trigonal Angle degree (%d) does not make sense ' + 'with %d coefficients' % (degree, len(coeffs))) + it = iter(data['AMOEBA_TRIGONAL_ANGLE_LIST']) + for i, j, k, l, m in zip(it, it, it, it, it): + self.trigonal_angles.append( + TrigonalAngle(self.atoms[i-1], self.atoms[j-1], + self.atoms[k-1], self.atoms[l-1], + self.trigonal_angle_types[m-1]) + ) + + #============================================= + + def _load_oopbend_info(self): + """ Loads the AMOEBA out-of-plane bending terms, if they exist """ + if not 'AMOEBA_OPBEND_ANGLE_LIST' in self.parm_data: return + data = self.parm_data + del self.out_of_plane_bends[:] + del self.out_of_plane_bend_types[:] + for k in data['AMOEBA_OPBEND_ANGLE_FORCE_CONSTANT']: + self.out_of_plane_bend_types.append( + OutOfPlaneBendType(k, self.out_of_plane_bend_types) + ) + self.out_of_plane_bend_types.degree = degree = \ + data['AMOEBA_OPBEND_ANGLE_FTAB_DEGREE'][0] + self.out_of_plane_bend_types.coeffs = coeffs = \ + data['AMOEBA_OPBEND_ANGLE_FTAB_COEFFS'][:] + if len(coeffs) != degree + 1: + raise AmberError('OOP-bend angle degree (%d) does not make sense ' + 'with %d coefficients.' % (degree, len(coeffs))) + it = iter(data['AMOEBA_OPBEND_ANGLE_LIST']) + for i, j, k, l, m in zip(it, it, it, it, it): + self.out_of_plane_bends.append( + OutOfPlaneBend(self.atoms[i-1], self.atoms[j-1], + self.atoms[k-1], self.atoms[l-1], + self.out_of_plane_bend_types[m-1]) + ) + + #============================================= + + def _load_dihedral_info(self): + """ Loads the AMOEBA regular torsion terms, if they exist """ + if not 'AMOEBA_TORSION_LIST' in self.parm_data: return + data = self.parm_data + del self.dihedrals[:] + del self.dihedral_types[:] + for k, per, phase in zip(data['AMOEBA_TORSION_FORCE_CONSTANT'], + data['AMOEBA_TORSION_PERIODICITY'], + data['AMOEBA_TORSION_PHASE']): + self.dihedral_types.append( + DihedralType(k, per, phase*RAD_TO_DEG, + list=self.dihedral_types) + ) + it = iter(data['AMOEBA_TORSION_LIST']) + for i, j, k, l, m in zip(it, it, it, it, it): + self.dihedrals.append( + Dihedral(self.atoms[i-1], self.atoms[j-1], self.atoms[k-1], + self.atoms[l-1], type=self.dihedral_types[m-1]) + ) + + #============================================= + + def _load_pitorsion_info(self): + """ Loads the AMOEBA pi-torsion terms, if they exist """ + if not 'AMOEBA_PI_TORSION_LIST' in self.parm_data: return + data = self.parm_data + del self.pi_torsions[:] + del self.pi_torsion_types[:] + for k, per, phase in zip(data['AMOEBA_PI_TORSION_FORCE_CONSTANT'], + data['AMOEBA_PI_TORSION_PERIODICITY'], + data['AMOEBA_PI_TORSION_PHASE']): + self.pi_torsion_types.append( + DihedralType(k, per, phase*RAD_TO_DEG, + list=self.pi_torsion_types) + ) + it = iter(data['AMOEBA_PI_TORSION_LIST']) + for i, j, k, l, m, n, o in zip(it, it, it, it, it, it, it): + self.pi_torsions.append( + PiTorsion(self.atoms[i-1], self.atoms[j-1], self.atoms[k-1], + self.atoms[l-1], self.atoms[m-1], self.atoms[n-1], + self.pi_torsion_types[o-1]) + ) + + #============================================= + + def _load_stretch_bend_info(self): + """ Loads the AMOEBA stretch-bend terms, if they exist """ + if not 'AMOEBA_STRETCH_BEND_LIST' in self.parm_data: return + data = self.parm_data + del self.stretch_bends[:] + del self.stretch_bend_types[:] + if 'AMOEBA_STRETCH_BEND_FORCE_CONSTANT' in data: + for a,b,c,d in zip(data['AMOEBA_STRETCH_BEND_FORCE_CONSTANT'], + data['AMOEBA_STRETCH_BEND_BOND1_EQUIL_VALUE'], + data['AMOEBA_STRETCH_BEND_BOND2_EQUIL_VALUE'], + data['AMOEBA_STRETCH_BEND_ANGLE_EQUIL_VALUE']): + self.stretch_bend_types.append( + StretchBendType(a, a, b, c, d, + list=self.stretch_bend_types) + ) + elif 'AMOEBA_STRETCH_BEND_FORCE_CONSTANT_1' in data: + for a,b,c,d,e in zip(data['AMOEBA_STRETCH_BEND_FORCE_CONSTANT_1'], + data['AMOEBA_STRETCH_BEND_FORCE_CONSTANT_2'], + data['AMOEBA_STRETCH_BEND_BOND1_EQUIL_VALUE'], + data['AMOEBA_STRETCH_BEND_BOND2_EQUIL_VALUE'], + data['AMOEBA_STRETCH_BEND_ANGLE_EQUIL_VALUE']): + self.stretch_bend_types.append( + StretchBendType(a, b, c, d, e, + list=self.stretch_bend_types) + ) + it = iter(data['AMOEBA_STRETCH_BEND_LIST']) + for i, j, k, l in zip(it, it, it, it): + self.stretch_bends.append( + StretchBend(self.atoms[i-1], self.atoms[j-1], + self.atoms[k-1], self.stretch_bend_types[l-1]) + ) + + #============================================= + + def _load_torsion_torsion_info(self): + """ Loads the AMOEBA coupled torsion-torsion terms, if they exist """ + if not 'AMOEBA_TORSION_TORSION_LIST' in self.parm_data: return + del self.torsion_torsion_types[:] + del self.torsion_torsions[:] + data = self.parm_data + ntypes = data['AMOEBA_TORSION_TORSION_NUM_PARAMS'][0] + for i in range(ntypes): + prefix = 'AMOEBA_TORSION_TORSION_TORTOR_TABLE_%02d_' % (i + 1) + dims = tuple(data[prefix + 'DIMS']) + ang1 = data[prefix + 'ANGLE1'] + ang2 = data[prefix + 'ANGLE2'] + f = data[prefix + 'FUNC'] + dfda1 = data[prefix + 'DFUNC_DANGLE1'] + dfda2 = data[prefix + 'DFUNC_DANGLE2'] + d2fda1da2 = data[prefix + 'D2FUNC_DANGLE1_DANGLE2'] + self.torsion_torsion_types.append( + TorsionTorsionType(dims, ang1, ang2, f, dfda1, + dfda2, d2fda1da2, + list=self.torsion_torsion_types) + ) + it = iter(data['AMOEBA_TORSION_TORSION_LIST']) + for i, j, k, l, m, n in zip(it, it, it, it, it, it): + self.torsion_torsions.append( + TorsionTorsion( + self.atoms[i-1], + self.atoms[j-1], + self.atoms[k-1], + self.atoms[l-1], + self.atoms[m-1], + self.torsion_torsion_types[n-1], + ) + ) + + #============================================= + + def _load_frame_info(self): + """ Loads the AMOEBA chiral and multipole frames """ + data = self.parm_data + del self.chiral_frames[:] + if 'AMOEBA_CHIRAL_FRAME_LIST' in data: + it = iter(data['AMOEBA_CHIRAL_FRAME_LIST']) + for i, j, k in zip(it, it, it): + self.chiral_frames.append( + ChiralFrame(self.atoms[i-1], self.atoms[j-1], k) + ) + del self.multipole_frames[:] + if 'AMOEBA_FRAME_DEF_LIST' in data: + it = iter(data['AMOEBA_FRAME_DEF_LIST']) + for i, j, k, l, m in zip(it, it, it, it, it): + self.multipole_frames.append( + MultipoleFrame(self.atoms[i-1], j, k, l, m) + ) + + #============================================= + + def _load_exception_info(self): + """ Loads all of the pairwise nonbonded exception rules """ + del self.adjust_types[:] + del self.adjusts[:] + data = self.parm_data + # This section should always be present + for a,b,c,d,e in zip(data['AMOEBA_ADJUST_VDW_WEIGHTS_LIST'], + data['AMOEBA_ADJUST_MPOLE_WEIGHTS_LIST'], + data['AMOEBA_ADJUST_DIRECT_WEIGHTS_LIST'], + data['AMOEBA_ADJUST_POLAR_WEIGHTS_LIST'], + data['AMOEBA_ADJUST_MUTUAL_WEIGHTS_LIST']): + self.adjust_types.append( + NonbondedExceptionType(a,b,c,d,e,list=self.adjust_types) + ) + it = iter(data['AMOEBA_ADJUST_LIST']) + for i, j, k in zip(it, it, it): + self.adjusts.append( + NonbondedException(self.atoms[i-1], self.atoms[j-1], self.adjust_types[k-1]) + ) + + #============================================= + + def _xfer_atom_info(self): + """ Transfers atom info to the topology file data arrays """ + data = self.parm_data + data['POINTERS'][PrmtopPointers.NATOM] = len(self.atoms) + self.pointers['NATOM'] = len(self.atoms) + data['ATOM_NAME'] = [a.name for a in self.atoms] + data['MASS'] = [a.mass for a in self.atoms] + data['CHARGE'] = [0.0 for a in self.atoms] # charge is in multipoles + data['AMBER_ATOM_TYPE'] = [a.type for a in self.atoms] + data['TREE_CHAIN_CLASSIFICATION'] = [a.tree for a in self.atoms] + data['JOIN_ARRAY'] = [a.join for a in self.atoms] + data['IROTAT'] = [a.irotat for a in self.atoms] + data['AMOEBA_ATOM_TYPE_INDEX'] = [a.type_idx for a in self.atoms] + data['AMOEBA_ATOMIC_NUMBER'] = [a.atomic_number for a in self.atoms] + data['AMOEBA_ATOM_CLASS_INDEX'] = [a.class_idx for a in self.atoms] + data['AMOEBA_VDW_ATOM_TYPES_LIST'] = [a.nb_idx for a in self.atoms] + data['AMOEBA_VDW_ATOM_PARENT_LIST'] = [a.vdw_parent.idx+1 for a in self.atoms] + data['AMOEBA_VDW_PARENT_COORD_WEIGHT_LIST'] = [a.vdw_weight for a in self.atoms] + data['AMOEBA_POLARIZABILITY_LIST'] = [a.polarizability for a in self.atoms] + data['AMOEBA_LOCAL_FRAME_MULTIPOLES_LIST'] = mpoles = [] + for atom in self.atoms: + mpoles.extend(atom.multipoles) + + #============================================= + + def _xfer_bond_info(self): + """ + Transfers the bond information from the bond arrays to the raw data + arrays + """ + if len(self.bonds) == 0: + self.delete_flag('AMOEBA_REGULAR_BOND_NUM_PARAMS') + self.delete_flag('AMOEBA_REGULAR_BOND_FORCE_CONSTANT') + self.delete_flag('AMOEBA_REGULAR_BOND_EQUIL_VALUE') + self.delete_flag('AMOEBA_REGULAR_BOND_FTAB_DEGREE') + self.delete_flag('AMOEBA_REGULAR_BOND_FTAB_COEFFS') + self.delete_flag('AMOEBA_REGULAR_BOND_NUM_LIST') + self.delete_flag('AMOEBA_REGULAR_BOND_LIST') + return + data = self.parm_data + for bond_type in self.bond_types: + bond_type.used = False + for bond in self.bonds: + bond.type.used = True + self.bond_types.prune_unused() + data['AMOEBA_REGULAR_BOND_NUM_PARAMS'] = [len(self.bond_types)] + data['AMOEBA_REGULAR_BOND_FORCE_CONSTANT'] = [bt.k for bt in self.bond_types] + data['AMOEBA_REGULAR_BOND_EQUIL_VALUE'] = [bt.req for bt in self.bond_types] + data['AMOEBA_REGULAR_BOND_FTAB_DEGREE'] = [self.bond_types.degree] + data['AMOEBA_REGULAR_BOND_FTAB_COEFFS'] = self.bond_types.coeffs[:] + data['AMOEBA_REGULAR_BOND_NUM_LIST'] = [len(self.bonds)] + data['AMOEBA_REGULAR_BOND_LIST'] = bond_array = [] + for bond in self.bonds: + bond_array.extend([bond.atom1.idx+1, bond.atom2.idx+1, bond.type.idx+1]) + + #============================================= + + def _xfer_angle_info(self): + """ + Transfers the regular AMOEBA angle information from the topology arrays + to the raw data arrays + """ + if len(self.angles) == 0: + self.delete_flag('AMOEBA_REGULAR_ANGLE_NUM_PARAMS') + self.delete_flag('AMOEBA_REGULAR_ANGLE_FORCE_CONSTANT') + self.delete_flag('AMOEBA_REGULAR_ANGLE_EQUIL_VALUE') + self.delete_flag('AMOEBA_REGULAR_ANGLE_FTAB_DEGREE') + self.delete_flag('AMOEBA_REGULAR_ANGLE_FTAB_COEFFS') + self.delete_flag('AMOEBA_REGULAR_ANGLE_NUM_LIST') + self.delete_flag('AMOEBA_REGULAR_ANGLE_LIST') + return + data = self.parm_data + for angle_type in self.angle_types: + angle_type.used = False + for angle in self.angles: + angle.type.used = True + self.angle_types.prune_unused() + data['AMOEBA_REGULAR_ANGLE_NUM_PARAMS'] = [len(self.angle_types)] + data['AMOEBA_REGULAR_ANGLE_FORCE_CONSTANT'] = [at.k for at in self.angle_types] + data['AMOEBA_REGULAR_ANGLE_EQUIL_VALUE'] = [at.theteq for at in self.angle_types] + data['AMOEBA_REGULAR_ANGLE_FTAB_DEGREE'] = [self.angle_types.degree] + data['AMOEBA_REGULAR_ANGLE_FTAB_COEFFS'] = self.angle_types.coeffs[:] + data['AMOEBA_REGULAR_ANGLE_NUM_LIST'] = [len(self.angles)] + data['AMOEBA_REGULAR_ANGLE_LIST'] = angle_array = [] + for angle in self.angles: + angle_array.extend([angle.atom1.idx+1, angle.atom2.idx+1, + angle.atom3.idx+1, angle.type.idx+1]) + + #============================================= + + def _xfer_urey_bradley_info(self): + """ + Transfers the AMOEBA Urey-Bradley bond information from the topology + arrays to the raw data arrays + """ + if len(self.urey_bradleys) == 0: + self.delete_flag('AMOEBA_UREY_BRADLEY_BOND_NUM_PARAMS') + self.delete_flag('AMOEBA_UREY_BRADLEY_BOND_FORCE_CONSTANT') + self.delete_flag('AMOEBA_UREY_BRADLEY_BOND_EQUIL_VALUE') + self.delete_flag('AMOEBA_UREY_BRADLEY_BOND_FTAB_DEGREE') + self.delete_flag('AMOEBA_UREY_BRADLEY_BOND_FTAB_COEFFS') + self.delete_flag('AMOEBA_UREY_BRADLEY_BOND_NUM_LIST') + self.delete_flag('AMOEBA_UREY_BRADLEY_BOND_LIST') + return + data = self.parm_data + for urey_bradley_type in self.urey_bradley_types: + urey_bradley_type.used = False + for urey_bradley in self.urey_bradleys: + urey_bradley.type.used = True + self.urey_bradley_types.prune_unused() + data['AMOEBA_UREY_BRADLEY_BOND_NUM_PARAMS'] = [len(self.urey_bradley_types)] + data['AMOEBA_UREY_BRADLEY_BOND_FORCE_CONSTANT'] = [ut.k for ut in self.urey_bradley_types] + data['AMOEBA_UREY_BRADLEY_BOND_EQUIL_VALUE'] = [ut.req for ut in self.urey_bradley_types] + data['AMOEBA_UREY_BRADLEY_BOND_FTAB_DEGREE'] = [self.urey_bradley_types.degree] + data['AMOEBA_UREY_BRADLEY_BOND_FTAB_COEFFS'] = self.urey_bradley_types.coeffs[:] + data['AMOEBA_UREY_BRADLEY_BOND_NUM_LIST'] = [len(self.urey_bradleys)] + data['AMOEBA_UREY_BRADLEY_BOND_LIST'] = urey_array = [] + for urey in self.urey_bradleys: + urey_array.extend([urey.atom1.idx+1, urey.atom2.idx+1, + urey.type.idx+1]) + + #============================================= + + def _xfer_trigonal_angle_info(self): + """ + Transfers the AMOEBA trigonal angle information from the topology arrays + to the raw data arrays + """ + if len(self.trigonal_angles) == 0: + self.delete_flag('AMOEBA_TRIGONAL_ANGLE_NUM_PARAMS') + self.delete_flag('AMOEBA_TRIGONAL_ANGLE_FORCE_CONSTANT') + self.delete_flag('AMOEBA_TRIGONAL_ANGLE_EQUIL_VALUE') + self.delete_flag('AMOEBA_TRIGONAL_ANGLE_FTAB_DEGREE') + self.delete_flag('AMOEBA_TRIGONAL_ANGLE_FTAB_COEFFS') + self.delete_flag('AMOEBA_TRIGONAL_ANGLE_NUM_LIST') + self.delete_flag('AMOEBA_TRIGONAL_ANGLE_LIST') + return + data = self.parm_data + for trigonal_angle_type in self.trigonal_angle_types: + trigonal_angle_type.used = False + for trigonal_angle in self.trigonal_angles: + trigonal_angle.type.used = True + self.trigonal_angle_types.prune_unused() + data['AMOEBA_TRIGONAL_ANGLE_NUM_PARAMS'] = [len(self.trigonal_angle_types)] + data['AMOEBA_TRIGONAL_ANGLE_FORCE_CONSTANT'] = [at.k for at in self.trigonal_angle_types] + data['AMOEBA_TRIGONAL_ANGLE_EQUIL_VALUE'] = [at.theteq for at in self.trigonal_angle_types] + data['AMOEBA_TRIGONAL_ANGLE_FTAB_DEGREE'] = [self.trigonal_angle_types.degree] + data['AMOEBA_TRIGONAL_ANGLE_FTAB_COEFFS'] = self.trigonal_angle_types.coeffs[:] + data['AMOEBA_TRIGONAL_ANGLE_NUM_LIST'] = [len(self.trigonal_angles)] + data['AMOEBA_TRIGONAL_ANGLE_LIST'] = angle_array = [] + for angle in self.trigonal_angles: + angle_array.extend([angle.atom1.idx+1, angle.atom2.idx+1, + angle.atom3.idx+1, angle.atom4.idx+1, + angle.type.idx+1]) + + #============================================= + + def _xfer_oopbend_info(self): + """ + Transfers the AMOEBA out-of-plane bending angle information from the + topology arrays to the raw data arrays + """ + if len(self.out_of_plane_bends) == 0: + self.delete_flag('AMOEBA_OPBEND_ANGLE_NUM_PARAMS') + self.delete_flag('AMOEBA_OPBEND_ANGLE_FORCE_CONSTANT') + self.delete_flag('AMOEBA_OPBEND_ANGLE_FTAB_DEGREE') + self.delete_flag('AMOEBA_OPBEND_ANGLE_FTAB_COEFFS') + self.delete_flag('AMOEBA_OPBEND_ANGLE_NUM_LIST') + self.delete_flag('AMOEBA_OPBEND_ANGLE_LIST') + return + data = self.parm_data + for out_of_plane_bend_type in self.out_of_plane_bend_types: + out_of_plane_bend_type.used = False + for out_of_plane_bend in self.out_of_plane_bends: + out_of_plane_bend.type.used = True + self.out_of_plane_bend_types.prune_unused() + data['AMOEBA_OPBEND_ANGLE_NUM_PARAMS'] = [len(self.out_of_plane_bend_types)] + data['AMOEBA_OPBEND_ANGLE_FORCE_CONSTANT'] = [at.k for at in self.out_of_plane_bend_types] + data['AMOEBA_OPBEND_ANGLE_FTAB_DEGREE'] = [self.out_of_plane_bend_types.degree] + data['AMOEBA_OPBEND_ANGLE_FTAB_COEFFS'] = self.out_of_plane_bend_types.coeffs[:] + data['AMOEBA_OPBEND_ANGLE_NUM_LIST'] = [len(self.out_of_plane_bends)] + data['AMOEBA_OPBEND_ANGLE_LIST'] = angle_array = [] + for angle in self.out_of_plane_bends: + angle_array.extend([angle.atom1.idx+1, angle.atom2.idx+1, + angle.atom3.idx+1, angle.atom4.idx+1, + angle.type.idx+1]) + + #============================================= + + def _xfer_dihedral_info(self): + """ + Transfers the AMOEBA regular torsion angle information from the topology + arrays to the raw data arrays + """ + if len(self.dihedrals) == 0: + self.delete_flag('AMOEBA_TORSION_NUM_PARAMS') + self.delete_flag('AMOEBA_TORSION_FORCE_CONSTANT') + self.delete_flag('AMOEBA_TORSION_PERIODICITY') + self.delete_flag('AMOEBA_TORSION_PHASE') + self.delete_flag('AMOEBA_TORSION_NUM_LIST') + self.delete_flag('AMOEBA_TORSION_LIST') + return + data = self.parm_data + for dihedral_type in self.dihedral_types: + dihedral_type.used = False + for dihedral in self.dihedrals: + dihedral.type.used = True + self.dihedral_types.prune_unused() + data['AMOEBA_TORSION_NUM_PARAMS'] = [len(self.dihedral_types)] + data['AMOEBA_TORSION_FORCE_CONSTANT'] = [dt.phi_k for dt in self.dihedral_types] + data['AMOEBA_TORSION_PEROIDICITY'] = [dt.per for dt in self.dihedral_types] + data['AMOEBA_TORSION_PHASE'] = [dt.phase*DEG_TO_RAD for dt in self.dihedral_types] + data['AMOEBA_TORSION_NUM_LIST'] = [len(self.dihedrals)] + data['AMOEBA_TORSION_LIST'] = dlist = [] + for dih in self.dihedrals: + dlist.extend([dih.atom1.idx+1, dih.atom2.idx+1, dih.atom3.idx+1, + dih.atom4.idx+1, dih.type.idx+1]) + + #============================================= + + def _xfer_pitorsion_info(self): + """ + Transfers the AMOEBA pi-torsion information from the topology arrays to + the raw data arrays + """ + if len(self.pi_torsions) == 0: + self.delete_flag('AMOEBA_PI_TORSION_NUM_PARAMS') + self.delete_flag('AMOEBA_PI_TORSION_FORCE_CONSTANT') + self.delete_flag('AMOEBA_PI_TORSION_PERIODICITY') + self.delete_flag('AMOEBA_PI_TORSION_PHASE') + self.delete_flag('AMOEBA_PI_TORSION_NUM_LIST') + self.delete_flag('AMOEBA_PI_TORSION_LIST') + return + data = self.parm_data + for pitor_type in self.pi_torsion_types: + pitor_type.used = False + for pi_torsion in self.pi_torsions: + pi_torsion.type.used = True + self.pi_torsion_types.prune_unused() + data['AMOEBA_PI_TORSION_NUM_PARAMS'] = [len(self.pi_torsion_types)] + data['AMOEBA_PI_TORSION_FORCE_CONSTANT'] = [dt.phi_k for dt in self.pi_torsion_types] + data['AMOEBA_PI_TORSION_PEROIDICITY'] = [dt.per for dt in self.pi_torsion_types] + data['AMOEBA_PI_TORSION_PHASE'] = [dt.phase*DEG_TO_RAD for dt in self.pi_torsion_types] + data['AMOEBA_PI_TORSION_NUM_LIST'] = [len(self.pi_torsions)] + data['AMOEBA_PI_TORSION_LIST'] = dlist = [] + for pit in self.pi_torsions: + dlist.extend([pit.atom1.idx+1, pit.atom2.idx+1, pit.atom3.idx+1, + pit.atom4.idx+1, pit.atom5.idx+1, pit.atom6.idx+1, + pit.type.idx+1]) + + #============================================= + + def _xfer_stretch_bend_info(self): + """ + Transfers the AMOEBA stretch-bend information from the topology arrays + to the raw data arrays + """ + if len(self.stretch_bends) == 0: + self.delete_flag('AMOEBA_STRETCH_BEND_FORCE_CONSTANT') + self.delete_flag('AMOEBA_STRETCH_BEND_FORCE_CONSTANT_1') + self.delete_flag('AMOEBA_STRETCH_BEND_FORCE_CONSTANT_2') + self.delete_flag('AMOEBA_STRETCH_BEND_BOND1_EQUIL_VALUE') + self.delete_flag('AMOEBA_STRETCH_BEND_BOND2_EQUIL_VALUE') + self.delete_flag('AMOEBA_STRETCH_BEND_ANGLE_EQUIL_VALUE') + self.delete_flag('AMOEBA_STRETCH_BEND_NUM_LIST') + self.delete_flag('AMOEBA_STRETCH_BEND_LIST') + return + # This flag is deprecated... get rid of it and replace it with the 2 + # force constant flags instead + # TODO: Deprecate the AMOEBA_STRETCH_BEND_FORCE_CONSTANT flag in + # tinker_to_amber and then do it here as well. +# self.delete_flag('AMOEBA_STRETCH_BEND_FORCE_CONSTANT') + data = self.parm_data + for strbnd_type in self.stretch_bend_types: + strbnd_type.used = False + for strbnd in self.stretch_bends: + strbnd.type.used = True + self.stretch_bend_types.prune_unused() + data['AMOEBA_STRETCH_BEND_NUM_PARAMS'] = [len(self.stretch_bend_types)] +# if not 'AMOEBA_STRETCH_BEND_FORCE_CONSTANT_1' in self.flag_list: +# self.add_flag('AMOEBA_STRETCH_BEND_FORCE_CONSTANT_1', '5E16.8', +# data=[strbnd.k1 for strbnd in self.stretch_bend_types]) +# else: +# data['AMOEBA_STRETCH_BEND_FORCE_CONSTANT_1'] = \ +# [strbnd.k1 for strbnd in self.stretch_bend_types] +# if not 'AMOEBA_STRETCH_BEND_FORCE_CONSTANT_2' in self.flag_list: +# self.add_flag('AMOEBA_STRETCH_BEND_FORCE_CONSTANT_2', '5E16.8', +# data=[strbnd.k2 for strbnd in self.stretch_bend_types]) +# else: +# data['AMOEBA_STRETCH_BEND_FORCE_CONSTANT_2'] = \ +# [strbnd.k2 for strbnd in self.stretch_bend_types] + data['AMOEBA_STRETCH_BEND_FORCE_CONSTANT'] = \ + [strbnd.k1 for strbnd in self.stretch_bend_types] + data['AMOEBA_STRETCH_BEND_BOND1_EQUIL_VALUE'] = \ + [strbnd.req1 for strbnd in self.stretch_bend_types] + data['AMOEBA_STRETCH_BEND_BOND2_EQUIL_VALUE'] = \ + [strbnd.req2 for strbnd in self.stretch_bend_types] + data['AMOEBA_STRETCH_BEND_ANGLE_EQUIL_VALUE'] = \ + [strbnd.theteq for strbnd in self.stretch_bend_types] + data['AMOEBA_STRETCH_BEND_NUM_LIST'] = [len(self.stretch_bends)] + data['AMOEBA_STRETCH_BEND_LIST'] = slist = [] + for strbnd in self.stretch_bends: + slist.extend([strbnd.atom1.idx+1, strbnd.atom2.idx+1, + strbnd.atom3.idx+1, strbnd.type.idx+1]) + + #============================================= + + def _xfer_torsion_torsion_info(self): + """ + Transfers the AMOEBA coupled torsion-torsion information from the + topology arrays to the raw data arrays + """ + if len(self.torsion_torsions) == 0: + delete_flags = set( + flag for flag in self.flag_list if flag.startswith('AMOEBA_TORSION_TORSION') + ) + for flag in delete_flags: + self.delete_flag(flag) + return + data = self.parm_data + for tortor_type in self.torsion_torsion_types: + tortor_type.used = False + for tortor in self.torsion_torsions: + tortor.type.used = True + self.torsion_torsion_types.prune_unused() + after = 'AMOEBA_TORSION_TORSION_NUM_PARAMS' + data[after] = [len(self.torsion_torsion_types)] + delete_flags = set(flag for flag in self.flag_list + if flag.startswith('AMOEBA_TORSION_TORSION_TORTOR_TABLE')) + for flag in delete_flags: + self.delete_flag(flag) + for i, tt in enumerate(self.torsion_torsion_types): + tblsize = ['dimension = (%d,%d)' % tt.dims] + prefix = 'AMOEBA_TORSION_TORSION_TORTOR_TABLE_%02d_' % (i+1) + self.add_flag(prefix+'DIMS', '2I8', data=list(tt.dims), + comments=['dimension = (2)'], after=after) + self.add_flag(prefix+'ANGLE1', '5E16.8', data=tt.ang1[:], + comments=['dimension = (%d)' % tt.dims[0]], after=prefix+'DIMS') + self.add_flag(prefix+'ANGLE2', '5E16.8', data=tt.ang2[:], + comments=['dimension = (%d)' % tt.dims[1]], after=prefix+'ANGLE1') + self.add_flag(prefix+'FUNC', '5E16.8', data=tt.f.data[:], + comments=tblsize[:], after=prefix+'ANGLE2') + self.add_flag(prefix+'DFUNC_DANGLE1', '5E16.8', data=tt.dfda1.data[:], + comments=tblsize[:], after=prefix+'FUNC') + self.add_flag(prefix+'DFUNC_DANGLE2', '5E16.8', data=tt.dfda2.data[:], + comments=tblsize[:], after=prefix+'DFUNC_DANGLE1') + self.add_flag(prefix+'D2FUNC_DANGLE1_DANGLE2', '5E16.8', data=tt.d2fda1da2.data[:], + comments=tblsize[:], after=prefix+'DFUNC_DANGLE2') + after = prefix + 'D2FUNC_DANGLE1_DANGLE2' + data['AMOEBA_TORSION_TORSION_NUM_LIST'] = [len(self.torsion_torsions)] + data['AMOEBA_TORSION_TORSION_LIST'] = tlist = [] + for tortor in self.torsion_torsions: + tlist.extend([tortor.atom1.idx+1, tortor.atom2.idx+1, + tortor.atom3.idx+1, tortor.atom4.idx+1, + tortor.atom5.idx+1, tortor.type.idx+1]) + + #============================================= + + def _xfer_frame_info(self): + """ + Transfers the chiral and multipole frame data from the topology arrays + to the raw data arrays + """ + data = self.parm_data + if len(self.chiral_frames) > 0: + data['AMOEBA_CHIRAL_FRAME_NUM_LIST'] = [len(self.chiral_frames)] + data['AMOEBA_CHIRAL_FRAME_LIST'] = clist = [] + for cf in self.chiral_frames: + clist.extend([cf.atom1.idx+1, cf.atom2.idx+1, cf.chirality]) + else: + self.delete_flag('AMOEBA_CHIRAL_FRAME_NUM_LIST') + self.delete_flag('AMOEBA_CHIRAL_FRAME_LIST') + + if len(self.multipole_frames) > 0: + data['AMOEBA_FRAME_DEF_NUM_LIST'] = [len(self.multipole_frames)] + data['AMOEBA_FRAME_DEF_LIST'] = flist = [] + for mf in self.multipole_frames: + flist.extend([mf.atom.idx+1, mf.frame_pt_num, mf.vectail, mf.vechead, mf.nvec]) + else: + self.delete_flag('AMOEBA_FRAME_DEF_NUM_LIST') + self.delete_flag('AMOEBA_FRAME_DEF_LIST') + + #============================================= + + def _xfer_exception_info(self): + """ + Transfers the nonboned exception (adjust) info from the topology arrays + to the raw data arrays + """ + # adjust type arrays hard-coded in length... do not purge unused. + data = self.parm_data + data['AMOEBA_ADJUST_VDW_WEIGHTS_LIST'] = [at.vdw_weight for at in self.adjust_types] + data['AMOEBA_ADJUST_MPOLE_WEIGHTS_LIST'] = [at.multipole_weight for at in self.adjust_types] + data['AMOEBA_ADJUST_DIRECT_WEIGHTS_LIST'] = [at.direct_weight for at in self.adjust_types] + data['AMOEBA_ADJUST_POLAR_WEIGHTS_LIST'] = [at.polar_weight for at in self.adjust_types] + data['AMOEBA_ADJUST_MUTUAL_WEIGHTS_LIST'] = [at.mutual_weight for at in self.adjust_types] + data['AMOEBA_ADJUST_NUM_LIST'] = [len(self.adjusts)] + data['AMOEBA_ADJUST_LIST'] = alist = [] + for adj in self.adjusts: + alist.extend([adj.atom1.idx + 1, adj.atom2.idx + 1, adj.type.idx + 1]) + +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +class BeemanRestart(AmberFormat): + """ + The restart files written for/by the Beeman integrator has the same type of + format as the topology file + """ + + @classmethod + def from_rawdata(cls, rawdata): + """ + Take the raw data from a AmberFormat object and initialize a + BeemanRestart from that data. + + Parameters + ---------- + rawdata : :class:`AmberFormat` + An AmberFormat instance that has already been instantiated + + Returns + ------- + inst : :class:`BeemanRestart` + An instance of this type from the data in rawdata + """ + inst = cls() + inst.name = rawdata.name + inst.version = rawdata.version + inst.formats = rawdata.formats + inst.parm_data = rawdata.parm_data + inst.parm_comments = rawdata.parm_comments + inst.flag_list = rawdata.flag_list + return inst + + @property + def natom(self): + return self.parm_data['ATOMIC_COORDS_NUM_LIST'][0] + + @natom.setter + def natom(self, value): + value = int(value) * 3 + old_natom3 = self.natom * 3 + if value > old_natom3: + # Getting bigger + zeros = [0 for i in range(value)] + new_data = zeros[:] + new_data[:old_natom3] = self.parm_data['ATOMIC_COORDS_LIST'] + self.parm_data['ATOMIC_COORDS_LIST'] = new_data + if 'ATOMIC_VELOCITIES_LIST' in self.parm_data: + new_data = zeros[:] + new_data[:old_natom3] = self.parm_data['ATOMIC_VELOCITIES_LIST'] + self.parm_data['ATOMIC_VELOCITIES_LIST'] = new_data + if 'ATOMIC_ACCELERATIONS_LIST' in self.parm_data: + new_data = zeros[:] + new_data[:old_natom3] = self.parm_data['ATOMIC_ACCELERATIONS_LIST'] + self.parm_data['ATOMIC_ACCELERATIONS_LIST'] = new_data + if 'OLD_ATOMIC_ACCELERATIONS_LIST' in self.parm_data: + new_data = zeros[:] + new_data[:old_natom3] = self.parm_data['OLD_ATOMIC_ACCELERATIONS_LIST'] + self.parm_data['OLD_ATOMIC_ACCELERATIONS_LIST'] = new_data + else: + # Getting smaller + del self.parm_data['ATOMIC_COORDS_LIST'][value:] + if 'ATOMIC_VELOCITIES_LIST' in self.parm_data: + del self.parm_data['ATOMIC_VELOCITIES_LIST'][value:] + if 'ATOMIC_ACCELERATIONS_LIST' in self.parm_data: + del self.parm_data['ATOMIC_ACCELERATIONS_LIST'][value:] + if 'OLD_ATOMIC_ACCELERATIONS_LIST' in self.parm_data: + del self.parm_data['OLD_ATOMIC_ACCELERATIONS_LIST'][value:] + value //= 3 + self.parm_data['ATOMIC_COORDS_NUM_LIST'][0] = value + if 'ATOMIC_VELOCITIES_LIST' in self.parm_data: + self.parm_data['ATOMIC_VELOCITIES_NUM_LIST'][0] = value + if 'ATOMIC_ACCELERATIONS_LIST' in self.parm_data: + self.parm_data['ATOMIC_ACCELERATIONS_NUM_LIST'][0] = value + if 'OLD_ATOMIC_ACCELERATIONS_NUM_LIST' in self.parm_data: + self.parm_data['OLD_ATOMIC_ACCELERATIONS_NUM_LIST'][0] = value + + @property + def coordinates(self): + return np.array(self.parm_data['ATOMIC_COORDS_LIST']).reshape((1, self.natom, 3)) + + @coordinates.setter + def coordinates(self, value): + value = np.asarray(value).flatten() + if value.shape != (3*self.natom,): + raise ValueError(f'Require {3 * self.natom}-length sequence for coordinates') + self.parm_data['ATOMIC_COORDS_LIST'] = value.tolist() + + @property + def velocities(self): + try: + return np.array(self.parm_data['ATOMIC_VELOCITIES_LIST']).reshape((1, self.natom, 3)) + except KeyError: + raise AttributeError('Beeman restart does not have velocities') + + @velocities.setter + def velocities(self, value): + value = np.asarray(value).flatten() + if value.shape != (3*self.natom,): + raise ValueError(f'Require {3 * self.natom}-length sequence for velocities') + if not 'ATOMIC_VELOCITIES_LIST' in self.flag_list: + self.add_flag('ATOMIC_VELOCITIES_NUM_LIST', 'i8', data=[self.natom]) + self.add_flag('ATOMIC_VELOCITIES_LIST', '3e20.12', data=value.tolist()) + else: + self.parm_data['ATOMIC_VELOCITIES_LIST'] = value.tolist() + + @property + def accelerations(self): + try: + return np.array( + self.parm_data['ATOMIC_ACCELERATIONS_LIST']).reshape((1, self.natom, 3) + ) + except KeyError: + raise AttributeError('Accelerations not present in Beeman restart') + + @accelerations.setter + def accelerations(self, value): + value = np.asarray(value).flatten() + if value.shape != (3*self.natom,): + raise ValueError(f'Require {3 * self.natom}-length sequence for accelerations') + if not 'ATOMIC_ACCELERATIONS_LIST' in self.flag_list: + self.add_flag('ATOMIC_ACCELERATIONS_NUM_LIST', 'i8', data=[self.natom]) + self.add_flag('ATOMIC_ACCELERATIONS_LIST', '3e20.12', data=value.tolist()) + else: + self.parm_data['ATOMIC_ACCELERATIONS_LIST'] = value.tolist() + + @property + def old_accelerations(self): + try: + return np.array( + self.parm_data['OLD_ATOMIC_ACCELERATIONS_LIST']).reshape((1, self.natom, 3) + ) + except KeyError: + raise AttributeError('Old accelerations not present in Beeman restart') + + @old_accelerations.setter + def old_accelerations(self, value): + value = np.asarray(value).flatten() + if value.shape != (3*self.natom,): + raise ValueError(f'Require {3 * self.natom}-length sequence for accelerations') + if not 'OLD_ATOMIC_ACCELERATIONS_LIST' in self.flag_list: + self.add_flag('OLD_ATOMIC_ACCELERATIONS_NUM_LIST', 'i8', data=[self.natom]) + self.add_flag('OLD_ATOMIC_ACCELERATIONS_LIST', '3e20.12', data=value.tolist()) + else: + self.parm_data['OLD_ATOMIC_ACCELERATIONS_LIST'] = value.tolist() + + @property + def box(self): + return np.array(self.parm_data['UNIT_CELL_PARAMETERS']) + + @box.setter + def box(self, stuff): + if len(stuff) != 6: + raise ValueError('Expected 3 box lengths and 3 box angles') + self.parm_data['UNIT_CELL_PARAMETERS'] = list(stuff) diff --git a/pmx/workflow/parmed/amber/amberformat.py b/pmx/workflow/parmed/amber/amberformat.py new file mode 100644 index 00000000..f26667b1 --- /dev/null +++ b/pmx/workflow/parmed/amber/amberformat.py @@ -0,0 +1,955 @@ +""" +This is a generalization of the readparm.AmberParm class to handle similar +Amber-style files with %FLAG/%FORMAT tags +""" +import datetime +import re +from contextlib import closing +from copy import copy +from math import ceil + +from ..constants import PrmtopPointers, AMBER_ELECTROSTATIC, CHARMM_ELECTROSTATIC +from ..exceptions import AmberError +from ..formats.registry import FileFormatType +from ..utils.io import genopen +from ..utils.fortranformat import FortranRecordReader, FortranRecordWriter + +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +class FortranFormat: + """ + Processes Fortran format strings according to the Fortran specification for + such formats. This object handles reading and writing data with any valid + Fortran format. It does this by using the `fortranformat` project + [https://bitbucket.org/brendanarnold/py-fortranformat]. + + However, while `fortranformat` is very general and adheres well to the + standard, it is very slow. As a result, simple, common format strings have + been optimized and processes reads and writes between 3 and 5 times faster. + The format strings (case-insensitive) of the following form (where # can be + replaced by any number) are optimized: + - #E#.# + - #D#.# + - #F#.# + - #(F#.#) + - #a# + - #I# + + Parameters + ---------- + format_string : str + The Fortran Format string to process + strip_strings : bool=True + If True, strings are stripped before being processed by stripping + (only) trailing whitespace + """ + + strre = re.compile(r'(\d+)?a(\d+)$', re.I) + intre = re.compile(r'(\d+)?i(\d+)$', re.I) + floatre = re.compile(r'(\d+)?[edf](\d+)\.(\d+)$', re.I) + floatre2 = re.compile(r'(\d+)?\([edf](\d+)\.(\d+)\)$', re.I) + + #=================================================== + + def __init__(self, format_string, strip_strings=True): + """ + Sets the format string and determines how we will read and write + strings using this format + """ + self.format = format_string + self.strip_strings = strip_strings # for ease of copying + + # Define a function that processes all arguments prior to adding them to + # the returned list. By default, do nothing, but this allows us to + # optionally strip whitespace from strings. + self.process_method = lambda x: x + + if FortranFormat.strre.match(format_string): + rematch = FortranFormat.strre.match(format_string) + # replace our write() method with write_string to force left-justify + self.type, self.write = str, self._write_string + nitems, itemlen = rematch.groups() + if nitems is None: + self.nitems = 1 + else: + self.nitems = int(nitems) + self.itemlen = int(itemlen) + self.fmt = '%s' + # See if we want to strip the strings + if strip_strings: self.process_method = lambda x: x.strip() + + elif FortranFormat.intre.match(format_string): + self.type = int + rematch = FortranFormat.intre.match(format_string) + nitems, itemlen = rematch.groups() + if nitems is None: + self.nitems = 1 + else: + self.nitems = int(nitems) + self.itemlen = int(itemlen) + self.fmt = '%%%dd' % self.itemlen + + elif FortranFormat.floatre.match(format_string): + self.type = float + rematch = FortranFormat.floatre.match(format_string) + nitems, itemlen, num_decimals = rematch.groups() + if nitems is None: + self.nitems = 1 + else: + self.nitems = int(nitems) + self.itemlen = int(itemlen) + self.num_decimals = int(num_decimals) + if 'F' in format_string.upper(): + self.fmt = '%%%s.%sF' % (self.itemlen, self.num_decimals) + else: + self.fmt = '%%%s.%sE' % (self.itemlen, self.num_decimals) + + elif FortranFormat.floatre2.match(format_string): + self.type = float + rematch = FortranFormat.floatre2.match(format_string) + nitems, itemlen, num_decimals = rematch.groups() + if nitems is None: + self.nitems = 1 + else: + self.nitems = int(nitems) + self.itemlen = int(itemlen) + self.num_decimals = int(num_decimals) + if 'F' in format_string.upper(): + self.fmt = '%%%s.%sF' % (self.itemlen, self.num_decimals) + else: + self.fmt = '%%%s.%sE' % (self.itemlen, self.num_decimals) + + else: + # We tried... now just use the fortranformat package + self._reader = FortranRecordReader(format_string) + self._writer = FortranRecordWriter(format_string) + self.write = self._write_ffwriter + self.read = self._read_ffreader + + #=================================================== + + def __copy__(self): + return type(self)(self.format, self.strip_strings) + + #=================================================== + + def __str__(self): + return self.format + + def __repr__(self): + return "<%s: %s>" % (type(self).__name__, self.format) + + #=================================================== + + def write(self, items, dest): + """ + Writes an iterable of data (or a single item) to the passed file-like + object + + Parameters + ---------- + items : iterable or single float/str/int + These are the objects to write in this format. The types of each + item should match the type specified in this Format for that + argument + dest : file or file-like + This is the file to write the data to. It must have a `write` method + or an AttributeError will be raised + + Notes + ----- + This method may be replaced with _write_string (for #a#-style formats) + or _write_ffwriter in the class initializer if no optimization is + provided for this format, but the call signatures and behavior are the + same for each of those functions. + """ + if hasattr(items, '__iter__') and not isinstance(items, str): + mod = self.nitems - 1 + for i, item in enumerate(items): + dest.write(self.fmt % item) + if i % self.nitems == mod: + dest.write('\n') + if i % self.nitems != mod: + dest.write('\n') + else: + dest.write(self.fmt % items) + dest.write('\n') + + #=================================================== + + def _write_string(self, items, dest): + """ Writes a list/tuple of strings """ + if hasattr(items, '__iter__') and not isinstance(items, str): + mod = self.nitems - 1 + for i, item in enumerate(items): + dest.write((self.fmt % item).ljust(self.itemlen)) + if i % self.nitems == mod: + dest.write('\n') + if i % self.nitems != mod: + dest.write('\n') + else: + dest.write((self.fmt % items).ljust(self.itemlen)) + dest.write('\n') + + #=================================================== + + def _read_nostrip(self, line): + """ + Reads the line and returns converted data. Special-cased for flags that + may contain 'blank' data. ugh. + """ + line = line.rstrip('\n') + nitems = int(ceil(len(line) / self.itemlen)) + ret = [0 for i in range(nitems)] + start, end = 0, self.itemlen + for i in range(nitems): + ret[i] = self.process_method(self.type(line[start:end])) + start = end + end += self.itemlen + return ret + + #=================================================== + + def read(self, line): + """ Reads the line and returns the converted data """ + line = line.rstrip() + nitems = int(ceil(len(line) / self.itemlen)) + ret = [0 for i in range(nitems)] + start, end = 0, self.itemlen + for i in range(nitems): + ret[i] = self.process_method(self.type(line[start:end])) + start = end + end += self.itemlen + return ret + + #=================================================== + + def _read_ffreader(self, line): + """ Reads the line and returns the converted data """ + return self._reader.read(line.rstrip()) + + #=================================================== + + def _write_ffwriter(self, items, dest): + dest.write('%s\n' % self._writer.write(items)) + + #=================================================== + + def __eq__(self, other): + return (self.format == other.format and + self.strip_strings == other.strip_strings) + + #=================================================== + + def __hash__(self): + return hash((self.format, self.strip_strings)) + + #=================================================== + + def __getstate__(self): + return dict(format=self.format, strip_strings=self.strip_strings) + + def __setstate__(self, d): + self.__init__(d['format'], d['strip_strings']) + +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +class AmberFormat(metaclass=FileFormatType): + """ + A class that can parse and print files stored in the Amber topology or MDL + format. In particular, these files have the general form: + + ``` + %VERSION VERSION_STAMP = V00001.000 DATE = XX/XX/XX XX:XX:XX + %FLAG + %COMMENT + %FORMAT() + ... data corresponding to that Fortran Format + %FLAG + %COMMENT + %FORMAT() + ... data corresponding to that Fortran Format + ``` + + where the `%COMMENT` sections are entirely optional + + Parameters + ---------- + fname : str=None + If provided, this file is parsed and the data structures will be loaded + from the data in this file + + Attributes + ---------- + parm_data : dict {str : list} + A dictionary that maps FLAG names to all of the data contained in that + section of the Amber file. + formats : dict {str : FortranFormat} + A dictionary that maps FLAG names to the FortranFormat instance in which + the data is stored in that section + parm_comments : dict {str : list} + A dictionary that maps FLAG names to the list of COMMENT lines that were + stored in the original file + flag_list : list + An ordered list of all FLAG names. This must be kept synchronized with + `parm_data`, `formats`, and `parm_comments` such that every item in + `flag_list` is a key to those 3 dicts and no other keys exist + charge_flag : str='CHARGE' + The name of the name of the FLAG that describes partial atomic charge + data. If this flag is found, then its data are multiplied by the + ELECTROSTATIC_CONSTANT to convert back to fractions of electrons + version : str + The VERSION string from the Amber file + name : str + The file name of the originally parsed file (set to the fname parameter) + """ + #=================================================== + + @staticmethod + def id_format(filename): + """ + Identifies the file type as either Amber-format file (like prmtop) or an + old-style topology file. + + Parameters + ---------- + filename : str + Name of the file to check format for + + Returns + ------- + is_fmt : bool + True if it is an Amber-style format, False otherwise + """ + if isinstance(filename, str): + with closing(genopen(filename, 'r')) as f: + lines = [f.readline() for i in range(5)] + elif all(hasattr(filename, attr) for attr in ['readline', 'seek', 'tell']): + cur = filename.tell() + lines = [filename.readline() for i in range(5)] + filename.seek(cur) + + if lines[0].startswith('%VERSION'): + return True + # Try old-style format + try: + return AmberFormat().rdparm_old(lines, check=True) + except ValueError: + return False + + #=================================================== + + @staticmethod + def parse(filename, *args, **kwargs): + """ + Meant for use with the automatic file loader, this will automatically + return a subclass of AmberFormat corresponding to what the information + in the prmtop file contains (i.e., either an AmberParm, ChamberParm, + AmoebaParm, or AmberFormat) + """ + from .readparm import LoadParm + from ._tinkerparm import BeemanRestart + try: + return LoadParm(filename, *args, **kwargs) + except (IndexError, KeyError): + parm = AmberFormat(filename, *args, **kwargs) + if 'ATOMIC_COORDS_LIST' in parm.parm_data: + return BeemanRestart.from_rawdata(parm) + return parm + + #=================================================== + + def __init__(self, fname=None): + """ Constructor. Read a file if given """ + self._ncopies = 0 + self.parm_data = {} + self.formats = {} + self.parm_comments = {} + self.flag_list = [] + self.version = None + self.charge_flag = 'CHARGE' + self.name = fname + + if fname is not None: + self.rdparm(fname) + + #=================================================== + + def __copy__(self): + """ Copy all of the data """ + self._ncopies += 1 + other = type(self)() + other.flag_list = self.flag_list[:] + other.version = self.version + if self.name is not None: + other.name = self.name + '_copy%d' % self._ncopies + else: + other.name = None + other.charge_flag = self.charge_flag + other.parm_data = {} + other.parm_comments = {} + other.formats = {} + for flag in other.flag_list: + other.parm_data[flag] = self.parm_data[flag][:] + other.parm_comments[flag] = self.parm_comments[flag][:] + other.formats[flag] = copy(self.formats[flag]) + return other + + #=================================================== + + def view_as(self, cls): + """ + Returns a view of the current object as another object. + + Parameters + ---------- + cls : type + Class definition of an AmberParm subclass for the current object to + be converted into + + Returns + ------- + instance of cls initialized from data in this object. This is NOT a deep + copy, so modifying the original object may modify this. The copy + function will create a deep copy of any AmberFormat-derived object + """ + # If these are the same classes, just return the original instance, + # since there's nothing to do. Classes are singletons, so use "is" + if type(self) is cls: + return self + return cls.from_rawdata(self) + + #=================================================== + + def rdparm(self, fname, slow=False): + """ Parses the Amber format file """ + self.name = fname + self.version = None # reset all top info each time rdparm is called + self.formats = {} + self.parm_data = {} + self.parm_comments = {} + self.flag_list = [] + + # See if we have the optimized parser available + try: + from . import _rdparm + except ImportError: + return self.rdparm_slow(fname) + + # The optimized parser only works on local, uncompressed files + # TODO: Add gzip and bzip2 support to the optimized reader + if (hasattr(fname, 'read') or slow + or fname.startswith('http://') or fname.startswith('https://') + or fname.startswith('ftp://') + or fname.endswith('.bz2') or fname.endswith('.gz')): + + return self.rdparm_slow(fname) + + # We have the optimized version and a local file + try: + ret = _rdparm.rdparm(fname) + except TypeError: + # This is raised if VERSION is not found + with closing(genopen(fname, 'r')) as f: + return self.rdparm_old(f.readlines()) + else: + # Unpack returned contents + parm_data, parm_comments, formats, unkflg, flag_list, version = ret + # Now assign them to instance attributes and process where necessary + self.parm_data = parm_data + self.parm_comments = parm_comments + for key in formats: + self.formats[key] = FortranFormat(formats[key]) + self.flag_list = flag_list + self.version = version + # Now we have to process all of those sections that the optimized + # parser couldn't figure out + for flag in unkflg: + rawdata = self.parm_data[flag] + self.parm_data[flag] = [] + for line in rawdata: + self.parm_data[flag].extend(self.formats[flag].read(line)) + if 'CTITLE' in self.parm_data: + CHARGE_SCALE = CHARMM_ELECTROSTATIC + else: + CHARGE_SCALE = AMBER_ELECTROSTATIC + try: + for i, chg in enumerate(self.parm_data[self.charge_flag]): + self.parm_data[self.charge_flag][i] = chg / CHARGE_SCALE + except KeyError: + pass + + #=================================================== + + def rdparm_slow(self, fname): + """ + Parses the Amber format file. This parser is written in pure Python and + is therefore slower than the C++-optimized version + """ + + current_flag = '' + fmtre = re.compile(r'%FORMAT *\((.+)\)') + version = None + + if isinstance(fname, str): + prm = genopen(fname, 'r') + own_handle = True + elif hasattr(fname, 'read'): + prm = fname + own_handle = False + else: + raise TypeError('%s must be a file name or file-like object' % fname) + + # Open up the file and read the data into memory + for line in prm: + if line[0] == '%': + if line[0:8] == '%VERSION': + self.version = line.strip() + continue + elif line[0:5] == '%FLAG': + current_flag = line[6:].strip() + self.formats[current_flag] = '' + self.parm_data[current_flag] = [] + self.parm_comments[current_flag] = [] + self.flag_list.append(current_flag) + continue + elif line[0:8] == '%COMMENT': + self.parm_comments[current_flag].append(line[9:].strip()) + continue + elif line[0:7] == '%FORMAT': + fmt = FortranFormat(fmtre.match(line).groups()[0]) + # RESIDUE_ICODE can have a lot of blank data... + if current_flag == 'RESIDUE_ICODE': + fmt.read = fmt._read_nostrip + self.formats[current_flag] = fmt + continue + try: + self.parm_data[current_flag].extend(fmt.read(line)) + except KeyError: + if version is not None: + raise + break # Skip out of the loop down to the old-format parser + + # convert charges to fraction-electrons + if 'CTITLE' in self.parm_data: + CHARGE_SCALE = CHARMM_ELECTROSTATIC + else: + CHARGE_SCALE = AMBER_ELECTROSTATIC + if self.charge_flag in self.parm_data: + for i, chg in enumerate(self.parm_data[self.charge_flag]): + self.parm_data[self.charge_flag][i] = chg / CHARGE_SCALE + # If we don't have a version, then read in an old-file topology + if self.version is None: + prm.seek(0) + return self.rdparm_old(prm.readlines()) + if own_handle: + prm.close() + return + + #=================================================== + + def rdparm_old(self, prmtop_lines, check=False): + """ + This reads an old-style topology file and stores the results in the + same data structures as a new-style topology file + + Parameters + ---------- + prmtop_lines : list of str + List of all lines in the prmtop file + check : bool, optional + If True, only the first couple sections will be read to determine if + this is, in fact, an old-style topology file + """ + def read_integer(line_idx, lines, num_items): + # line_idx should be the line _before_ the first line you + # want data from. + i, tmp_data = 0, [] + while i < num_items: + idx = i % 12 + if idx == 0: + line_idx += 1 + try: + tmp_data.append(int(lines[line_idx][idx*6:idx*6+6])) + except ValueError: + raise ValueError( + 'Error parsing line %d, token %d [%s]: Problem ' + 'during integer read.' % (line_idx, idx, + lines[line_idx][idx*6:idx*6+6]) + ) + i += 1 + # If we had no items, we need to jump a line: + if num_items == 0: line_idx += 1 + return tmp_data, line_idx + + def read_string(line_idx, lines, num_items): + # line_idx should be the line _before_ the first line you + # want data from. + i, tmp_data = 0, [] + while i < num_items: + idx = i % 20 + if idx == 0: + line_idx += 1 + tmp_data.append(lines[line_idx][idx*4:idx*4+4]) + i += 1 + # If we had no items, we need to jump a line: + if num_items == 0: line_idx += 1 + return tmp_data, line_idx + + def read_float(line_idx, lines, num_items): + # line_idx should be the line _before_ the first line you + # want data from. + i, tmp_data = 0, [] + while i < num_items: + idx = i % 5 + if idx == 0: + line_idx += 1 + try: + tmp_data.append(float(lines[line_idx][idx*16:idx*16+16])) + except ValueError: + raise ValueError( + 'Error parsing line %d, token %d [%s]: Problem ' + 'during floating point read.' % (line_idx, idx, + lines[line_idx][idx*16:idx*16+16]) + ) + i += 1 + # If we had no items, we need to jump a line: + if num_items == 0: line_idx += 1 + return tmp_data, line_idx + + # First add a title + self.add_flag('TITLE', '20a4', data=['| Converted old-style topology']) + + # Next, read in the pointers + line_idx = 0 + tmp_data, line_idx = read_integer(line_idx, prmtop_lines, 30) + # Add a final pointer of 0, which corresponds to NUMEXTRA + tmp_data.append(0) + self.add_flag('POINTERS', '10I8', data=tmp_data) + + # Set some of the pointers we need + natom = self.parm_data['POINTERS'][PrmtopPointers.NATOM] + ntypes = self.parm_data['POINTERS'][PrmtopPointers.NTYPES] + nres = self.parm_data['POINTERS'][PrmtopPointers.NRES] + numbnd = self.parm_data['POINTERS'][PrmtopPointers.NUMBND] + numang = self.parm_data['POINTERS'][PrmtopPointers.NUMANG] + nptra = self.parm_data['POINTERS'][PrmtopPointers.NPTRA] + natyp = self.parm_data['POINTERS'][PrmtopPointers.NATYP] + nbonh = self.parm_data['POINTERS'][PrmtopPointers.NBONH] + nbona = self.parm_data['POINTERS'][PrmtopPointers.NBONA] + ntheth = self.parm_data['POINTERS'][PrmtopPointers.NTHETH] + ntheta = self.parm_data['POINTERS'][PrmtopPointers.NTHETA] + nex = self.parm_data['POINTERS'][PrmtopPointers.NEXT] + nphia = self.parm_data['POINTERS'][PrmtopPointers.NPHIA] + nphb = self.parm_data['POINTERS'][PrmtopPointers.NPHB] + nphih = self.parm_data['POINTERS'][PrmtopPointers.NPHIH] + + # This is enough to convince me that we have an old-style prmtop if we + # have the number of integers I suspect we should + if check: + return len(tmp_data) == 31 + + # Next read in the atom names + tmp_data, line_idx = read_string(line_idx, prmtop_lines, natom) + self.add_flag('ATOM_NAME', '20a4', data=tmp_data) + + # Next read the charges + tmp_data, line_idx = read_float(line_idx, prmtop_lines, natom) + # Divide by the electrostatic constant + tmp_data = [x / AMBER_ELECTROSTATIC for x in tmp_data] + self.add_flag('CHARGE', '5E16.8', data=tmp_data) + + # Next read the masses + tmp_data, line_idx = read_float(line_idx, prmtop_lines, natom) + self.add_flag('MASS', '5E16.8', data=tmp_data) + + # Next read atom type index + tmp_data, line_idx = read_integer(line_idx, prmtop_lines, natom) + self.add_flag('ATOM_TYPE_INDEX', '10I8', data=tmp_data) + + # Next read number excluded atoms + tmp_data, line_idx = read_integer(line_idx, prmtop_lines, natom) + self.add_flag('NUMBER_EXCLUDED_ATOMS', '10I8', data=tmp_data) + + # Next read nonbonded parm index + tmp_data, line_idx = read_integer(line_idx, prmtop_lines, ntypes**2) + self.add_flag('NONBONDED_PARM_INDEX', '10I8', data=tmp_data) + + # Next read residue label + tmp_data, line_idx = read_string(line_idx, prmtop_lines, nres) + self.add_flag('RESIDUE_LABEL', '20a4', data=tmp_data) + + # Next read residue pointer + tmp_data, line_idx = read_integer(line_idx, prmtop_lines, nres) + self.add_flag('RESIDUE_POINTER', '10I8', data=tmp_data) + + # Next read bond force constant + tmp_data, line_idx = read_float(line_idx, prmtop_lines, numbnd) + self.add_flag('BOND_FORCE_CONSTANT', '5E16.8', data=tmp_data) + + # Next read bond equil value + tmp_data, line_idx = read_float(line_idx, prmtop_lines, numbnd) + self.add_flag('BOND_EQUIL_VALUE', '5E16.8', data=tmp_data) + + # Next read angle force constant + tmp_data, line_idx = read_float(line_idx, prmtop_lines, numang) + self.add_flag('ANGLE_FORCE_CONSTANT', '5E16.8', data=tmp_data) + + # Next read the angle equilibrium value + tmp_data, line_idx = read_float(line_idx, prmtop_lines, numang) + self.add_flag('ANGLE_EQUIL_VALUE', '5E16.8', data=tmp_data) + + # Next read the dihedral force constant + tmp_data, line_idx = read_float(line_idx, prmtop_lines, nptra) + self.add_flag('DIHEDRAL_FORCE_CONSTANT', '5E16.8', data=tmp_data) + + # Next read dihedral periodicity + tmp_data, line_idx = read_float(line_idx, prmtop_lines, nptra) + self.add_flag('DIHEDRAL_PERIODICITY', '5E16.8', data=tmp_data) + + # Next read the dihedral phase + tmp_data, line_idx = read_float(line_idx, prmtop_lines, nptra) + self.add_flag('DIHEDRAL_PHASE', '5E16.8', data=tmp_data) + + # Next read SOLTY (?) + tmp_data, line_idx = read_float(line_idx, prmtop_lines, natyp) + self.add_flag('SOLTY', '5E16.8', data=tmp_data) + + # Next read lennard jones acoef and bcoef + numvals = ntypes * (ntypes + 1) / 2 + tmp_data, line_idx = read_float(line_idx, prmtop_lines, numvals) + self.add_flag('LENNARD_JONES_ACOEF', '5E16.8', data=tmp_data) + tmp_data, line_idx = read_float(line_idx, prmtop_lines, numvals) + self.add_flag('LENNARD_JONES_BCOEF', '5E16.8', data=tmp_data) + + # Next read bonds including hydrogen + tmp_data, line_idx = read_integer(line_idx, prmtop_lines, nbonh*3) + self.add_flag('BONDS_INC_HYDROGEN', '10I8', data=tmp_data) + + # Next read bonds without hydrogen + tmp_data, line_idx = read_integer(line_idx, prmtop_lines, nbona*3) + self.add_flag('BONDS_WITHOUT_HYDROGEN', '10I8', data=tmp_data) + + # Next read angles including hydrogen + tmp_data, line_idx = read_integer(line_idx, prmtop_lines, ntheth*4) + self.add_flag('ANGLES_INC_HYDROGEN', '10I8', data=tmp_data) + + # Next read angles without hydrogen + tmp_data, line_idx = read_integer(line_idx, prmtop_lines, ntheta*4) + self.add_flag('ANGLES_WITHOUT_HYDROGEN', '10I8', data=tmp_data) + + # Next read dihdrals including hydrogen + tmp_data, line_idx = read_integer(line_idx, prmtop_lines, nphih*5) + self.add_flag('DIHEDRALS_INC_HYDROGEN', '10I8', data=tmp_data) + + # Next read dihedrals without hydrogen + tmp_data, line_idx = read_integer(line_idx, prmtop_lines, nphia*5) + self.add_flag('DIHEDRALS_WITHOUT_HYDROGEN', '10I8', data=tmp_data) + + # Next read the excluded atoms list + tmp_data, line_idx = read_integer(line_idx, prmtop_lines, nex) + self.add_flag('EXCLUDED_ATOMS_LIST', '10I8', data=tmp_data) + + # Next read the hbond terms + tmp_data, line_idx = read_float(line_idx, prmtop_lines, nphb) + self.add_flag('HBOND_ACOEF', '5E16.8', data=tmp_data) + tmp_data, line_idx = read_float(line_idx, prmtop_lines, nphb) + self.add_flag('HBOND_BCOEF', '5E16.8', data=tmp_data) + tmp_data, line_idx = read_float(line_idx, prmtop_lines, nphb) + self.add_flag('HBCUT', '5E16.8', data=tmp_data) + + # Next read amber atom type + tmp_data, line_idx = read_string(line_idx, prmtop_lines, natom) + self.add_flag('AMBER_ATOM_TYPE', '20a4', data=tmp_data) + + # Next read tree chain classification + tmp_data, line_idx = read_string(line_idx, prmtop_lines, natom) + self.add_flag('TREE_CHAIN_CLASSIFICATION', '20a4', data=tmp_data) + + # Next read the join array + tmp_data, line_idx = read_integer(line_idx, prmtop_lines, natom) + self.add_flag('JOIN_ARRAY', '10I8', data=tmp_data) + + # Next read the irotat array + tmp_data, line_idx = read_integer(line_idx, prmtop_lines, natom) + self.add_flag('IROTAT', '10I8', data=tmp_data) + + # Now do PBC stuff + if self.parm_data['POINTERS'][PrmtopPointers.IFBOX]: + # Solvent pointers + tmp_data, line_idx = read_integer(line_idx, prmtop_lines, 3) + self.add_flag('SOLVENT_POINTERS', '10I8', data=tmp_data) + nspm = tmp_data[1] + + # Atoms per molecule + tmp_data, line_idx = read_integer(line_idx, prmtop_lines, nspm) + self.add_flag('ATOMS_PER_MOLECULE', '10I8', data=tmp_data) + + # Box dimensions + tmp_data, line_idx = read_float(line_idx, prmtop_lines, 4) + self.add_flag('BOX_DIMENSIONS', '5E16.8', data=tmp_data) + + # Now do CAP stuff + if self.parm_data['POINTERS'][PrmtopPointers.IFCAP]: + # CAP_INFO + tmp_data, line_idx = read_integer(line_idx, prmtop_lines, 1) + self.add_flag('CAP_INFO', '10I8', data=tmp_data) + tmp_data, line_idx = read_integer(line_idx, prmtop_lines, 4) + self.add_flag('CAP_INFO2', '10I8', data=tmp_data) + # end if self.parm_data['POINTERS'][IFCAP] + + #=================================================== + + def set_version(self): + """ Sets the version string """ + now = datetime.datetime.now() + self.version = ( + '%%VERSION VERSION_STAMP = V0001.000 DATE = %02d/%02d/%02d ' + '%02d:%02d:%02d' % (now.month, now.day, now.year % 100, + now.hour, now.minute, now.second) + ) + + #=================================================== + + def write_parm(self, name): + """ + Writes the current data in parm_data into a new topology file with + the given name + + Parameters + ---------- + name : str or file-like + Name of the file to write the topology file to or file-like object to write + """ + # now that we know we will write the new prmtop file, open the new file + if isinstance(name, str): + new_prm = genopen(name, 'w') + own_handle = True + else: + new_prm = name + own_handle = False + try: + # get current time to put into new prmtop file if we had a %VERSION + self.set_version() + # convert charges back to amber charges... + if 'CTITLE' in self.parm_data: + CHARGE_SCALE = CHARMM_ELECTROSTATIC + else: + CHARGE_SCALE = AMBER_ELECTROSTATIC + + if self.charge_flag in self.parm_data.keys(): + for i in range(len(self.parm_data[self.charge_flag])): + self.parm_data[self.charge_flag][i] *= CHARGE_SCALE + # write version to top of prmtop file + new_prm.write('%s\n' % self.version) + + # write data to prmtop file, inserting blank line if it's an empty field + for flag in self.flag_list: + new_prm.write('%%FLAG %s\n' % flag) + # Insert any comments before the %FORMAT specifier + for comment in self.parm_comments[flag]: + new_prm.write('%%COMMENT %s\n' % comment) + new_prm.write('%%FORMAT(%s)\n' % self.formats[flag]) + if len(self.parm_data[flag]) == 0: # empty field... + new_prm.write('\n') + continue + self.formats[flag].write(self.parm_data[flag], new_prm) + finally: + if own_handle: + new_prm.close() + + if self.charge_flag in self.parm_data.keys(): + # Convert charges back to electron-units + for i in range(len(self.parm_data[self.charge_flag])): + self.parm_data[self.charge_flag][i] /= CHARGE_SCALE + + #=================================================== + + def add_flag(self, flag_name, flag_format, data=None, num_items=-1, + comments=None, after=None): + """ + Adds a new flag with the given flag name and Fortran format string and + initializes the array with the values given, or as an array of 0s + of length num_items + + Parameters + ---------- + flag_name : str + Name of the flag to insert. It is converted to all upper case + flag_format : str + Fortran format string representing how the data in this section + should be written and read. Do not enclose in () + data : list=None + Sequence with data for the new flag. If None, a list of zeros of + length ``num_items`` (see below) is given as a holder + num_items : int=-1 + Number of items in the section. This variable is ignored if a set of + data are given in `data` + comments : list of str=None + List of comments to add to this section + after : str=None + If provided, the added flag will be added after the one with the + name given to `after`. If this flag does not exist, IndexError will + be raised + + Raises + ------ + AmberError if flag already exists + IndexError if the ``after`` flag does not exist + """ + if flag_name in self.parm_data: + raise AmberError('%s already exists' % (flag_name)) + if after is not None: + after = after.upper() + if not after in self.flag_list: + raise IndexError('%s not found in topology flag list' % after) + # If the 'after' flag is the last one, just append + if self.flag_list[-1] == after: + self.flag_list.append(flag_name.upper()) + else: + # Otherwise find the index and add it after + idx = self.flag_list.index(after) + 1 + self.flag_list.insert(idx, flag_name.upper()) + else: + self.flag_list.append(flag_name.upper()) + self.formats[flag_name.upper()] = FortranFormat(flag_format) + if data is not None: + self.parm_data[flag_name.upper()] = list(data) + else: + if num_items < 0: + raise AmberError("If you do not supply prmtop data, num_items " + "must be non-negative!") + self.parm_data[flag_name.upper()] = [0 for i in range(num_items)] + if comments is not None: + if isinstance(comments, str): + comments = [comments] + else: + comments = list(comments) + self.parm_comments[flag_name.upper()] = comments + else: + self.parm_comments[flag_name.upper()] = [] + + #=================================================== + + def delete_flag(self, flag_name): + """ Removes a flag from the topology file """ + flag_name = flag_name.upper() + if flag_name in self.flag_list: + del self.flag_list[self.flag_list.index(flag_name)] + if flag_name in self.parm_comments: + del self.parm_comments[flag_name] + if flag_name in self.formats: + del self.formats[flag_name] + if flag_name in self.parm_data: + del self.parm_data[flag_name] + + #=================================================== + + def __getstate__(self): + return dict(parm_data=self.parm_data, flag_list=self.flag_list, + formats=self.formats, parm_comments=self.parm_comments, + charge_flag=self.charge_flag, version=self.version, + name=self.name) + + def __setstate__(self, d): + self.__dict__ = d diff --git a/pmx/workflow/parmed/amber/asciicrd.py b/pmx/workflow/parmed/amber/asciicrd.py new file mode 100644 index 00000000..dd0fd07f --- /dev/null +++ b/pmx/workflow/parmed/amber/asciicrd.py @@ -0,0 +1,633 @@ +""" +This is a pure-python module for reading and writing ASCII Amber structure +files, like trajectories and restarts. Because it is pure-python and +formatted-ASCII, parsing and writing these files are expected to be slow. Binary +alternatives (like DCD and NetCDF, provided in netcdffiles.py) are strongly +encouraged, but these are provided for more complete compatibility and for +instances where the prequisites may not be installed. +""" +from math import ceil +import numpy as np +from ..formats.registry import FileFormatType +from ..utils.io import genopen +from ..structure import Structure +from ..topologyobjects import Atom +from .. import unit as u +from ..vec3 import Vec3 +import warnings as _warnings + +VELSCALE = 20.455 +ONEVELSCALE = 1 / VELSCALE + +class _FileEOF(Exception): + """ For control flow """ + +class _AmberAsciiCoordinateFile(metaclass=FileFormatType): + """ + Abstract base class for interacting with ASCII coordinate files. + Opens a new ASCII coordinate file and either parses it (loading + everything into memory) or sets it up for writing. + + Parameters + ---------- + fname : str + File name to open + natom : int + Number of atoms in the system + hasbox : bool + Does the system have PBCs? + mode : str={'r', 'w'} + Whether to open this file for 'r'eading or 'w'riting + title : str, optional + Title to write to a new trajectory (when mode='w') + """ + + DEFAULT_TITLE = None + CRDS_PER_LINE = None + + def __init__(self, fname, natom, hasbox, mode='r', title=None): + if mode == 'r': + self._status = 'old' + elif mode == 'w': + self._status = 'new' + # We need to have some way to know whether we need to write the + # coordinates or the box for this particular frame. Each frame must + # be written as coordinates first, then box. + self._writebox = False + else: + raise ValueError(f"{self.__class__.__name__} mode must be 'r' or 'w'") + if isinstance(fname, str): + self._file = genopen(fname, mode) + self._own_handle = True + elif hasattr(fname, 'read'): + self._file = fname + self._own_handle = False + else: + raise TypeError(f"Unsupported type for {fname}") + + self.natom = natom + self.hasbox = hasbox + if self.CRDS_PER_LINE is None: + raise NotImplementedError("CRDS_PER_LINE must be set by the subclass") + self._full_lines_per_frame = self.natom * 3 // self.CRDS_PER_LINE + self._nextras = self.natom * 3 - (self._full_lines_per_frame * self.CRDS_PER_LINE) + if self._own_handle: + self.closed = False + if self._status == 'old': + self._parse() + elif self._status == 'new': + if title is None: + self._file.write('%s\n' % self.DEFAULT_TITLE) + else: + self._file.write(title.rstrip() + '\n') + + def _parse(self): + """ Handles actual file parsing """ + raise NotImplementedError('virtual method not overwritten') + + @property + def positions(self): + return [Vec3(*xyz) for xyz in self._coordinates[0]] * u.angstroms + + def close(self): + """ Close the open file handler """ + if self._own_handle: + self.closed or self._file.close() + self.closed = True + + def __del__(self): + """ Make sure the open file handler is closed """ + try: + self.closed or self._file.close() + except AttributeError: + pass + +#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +class AmberAsciiRestart(_AmberAsciiCoordinateFile): + """ + Parser for the Amber ASCII inpcrd/restart file format + + Parameters + ---------- + fname : str + File name to open + mode : str={'r', 'w'} + Whether to open this file for 'r'eading or 'w'riting + natom : int, optional + Number of atoms in the system (necessary when mode='w') + hasbox : bool, optional + Does the system have PBCs? Necessary when mode='w' + title : str, optional + Title to write to a new trajectory (when mode='w') + time : float, optional + The time to write to the restart file in ps. Default is 0. + """ + @staticmethod + def id_format(filename): + """ Identifies the file type as an Amber restart/inpcrd file + + Parameters + ---------- + filename : str + Name of the file to check format for + + Returns + ------- + is_fmt : bool + True if it is an Amber restart/inpcrd file. False otherwise + """ + if isinstance(filename, str): + f = genopen(filename, 'r') + lines = [f.readline() for i in range(5)] + f.close() + elif all(hasattr(filename, attr) for attr in ['readline', 'seek', 'tell']): + cur = filename.tell() + lines = [filename.readline() for i in range(5)] + filename.seek(cur) + # Look for natom + words = lines[1].split() + if len(words) > 2 or len(words) < 1: + return False + try: + natom = int(words[0]) + float(words[1]) + except ValueError: + return False + except IndexError: + pass + # Next 3 lines, make sure we have %12.7f format. This only works if we + # have at least 6 atoms. Any fewer than that means the restart file is + # shorter than that. + try: + if natom <= 0: + return False + i = 0 + for line in lines[2:]: + i += 1 + if i > natom: break + for j in range(3): + j12 = j * 12 + if line[j12+4] != '.': return False + float(line[j12:j12+12]) + if line[j12+11] not in '0123456789': + return False + i += 1 + if i > natom: break + for j in range(3): + j12 = j * 12 + 36 + if line[j12+4] != '.': return False + float(line[j12:j12+12]) + if line[j12+11] not in '0123456789': + return False + except (IndexError, ValueError): + return False + + # Must be a restart... + return True + + CRDS_PER_LINE = 6 + DEFAULT_TITLE = 'restart created by ParmEd' + + def __init__(self, fname, mode='r', natom=0, hasbox=None, title=None, time=0.0): + """ + For restart files, natom and hasbox are determined automatically for + mode='r', and can be determined at write-time when the coordinates are + set. + """ + self._coords_written = False + self._cell_lengths_written = False + self._cell_angles_written = False + self._vels_written = False + self.time = float(time) + super().__init__(fname, natom, hasbox, mode, title) + + @classmethod + def parse(cls, filename, structure=False): + self = cls(filename) + if structure: + obj = Structure() + for _ in range(self.natom): + # fake + obj.add_atom(Atom(), resname='XXX', resnum=0) + obj.box = self.box + obj.coordinates = self.coordinates[0] + obj._coordinates = self.coordinates + return obj + else: + return self + + def _parse(self): + """ + This method parses the data out of the ASCII restart file and creates + self._coordinates and self._velocities as np.ndarray(natom*3) arrays + + This method is called automatically for 'old' restart files and should + not be called by external callers + """ + + lines = self._file.readlines() + self._file.close() + self.title = lines[0].strip() + self.natom = int(lines[1].strip().split()[0]) + try: + self.time = float(lines[1].strip().split()[1]) + except IndexError: + self.time = 0.0 + # Get rid of any trailing newlines + while not lines[-1].strip(): + lines.pop() + # Determine what information we have based on the number of lines + # present + if len(lines) == int(ceil(self.natom / 2.0) + 2): + self.hasbox = self.hasvels = False + elif len(lines) == int(ceil(self.natom / 2.0) + 3): + self.hasbox = True + self.hasvels = False + elif len(lines) == int(2 * ceil(self.natom / 2.0) + 2): + self.hasbox = False + self.hasvels = True + elif len(lines) == int(2 * ceil(self.natom / 2.0) + 3): + self.hasbox = self.hasvels = True + else: + raise RuntimeError(f'Badly formatted restart file. Has {len(lines)} lines for {self.natom} atoms.') + self._coordinates = np.zeros((self.natom, 3)) + if self.hasvels: + self._velocities = np.zeros((self.natom, 3)) + if self.hasbox: + self._cell_lengths = np.zeros(3) + self._cell_angles = np.zeros(3) + # Now it's time to parse. Coordinates first + startline = 2 + endline = startline + int(ceil(self.natom / 2.0)) + idx = 0 + for i in range(startline, endline): + line = lines[i] + x1 = float(line[ 0:12]) + y1 = float(line[12:24]) + z1 = float(line[24:36]) + self._coordinates[idx] = [x1, y1, z1] + idx += 1 + try: + x2 = float(line[36:48]) + y2 = float(line[48:60]) + z2 = float(line[60:72]) + except ValueError: + pass + else: + self._coordinates[idx] = [x2, y2, z2] + idx += 1 + self._coordinates = self._coordinates.reshape((1, self.natom, 3)) + startline = endline + # Now it's time to parse the velocities if we have them + if self.hasvels: + endline = startline + int(ceil(self.natom / 2.0)) + idx = 0 + for i in range(startline, endline): + line = lines[i] + x1 = float(line[ 0:12]) * VELSCALE + y1 = float(line[12:24]) * VELSCALE + z1 = float(line[24:36]) * VELSCALE + self._velocities[idx] = [x1, y1, z1] + idx += 1 + try: + x2 = float(line[36:48]) * VELSCALE + y2 = float(line[48:60]) * VELSCALE + z2 = float(line[60:72]) * VELSCALE + except ValueError: + pass + else: + self._velocities[idx] = [x2,y2,z2] + idx += 1 + startline = endline + self._velocities = self._velocities.reshape((1, self.natom, 3)) + # Now it's time to parse the box info if we have it + if self.hasbox: + line = lines[startline] + self._cell_lengths[0:3] = [float(line[0:12]), float(line[12:24]), float(line[24:36])] + self._cell_angles[0:3] = [float(line[36:48]), float(line[48:60]), float(line[60:72])] + + @property + def coordinates(self): + if self._status == 'new' and not hasattr(self, '_coordinates'): + raise RuntimeError('Coordinates not yet set') + return self._coordinates + + @coordinates.setter + def coordinates(self, stuff): + if self._status == 'old': + raise RuntimeError('Cannot set coordinates on an old restart') + stuff = np.array(stuff, copy=False).ravel() + if self.natom > 0 and len(stuff) != 3 * self.natom: + raise ValueError(f'Got {len(stuff)} coordinates for {self.natom} atoms') + if self._coords_written: + raise RuntimeError('Coordinates have already been written.') + # Error checking done. If we didn't already set our number of atoms, + # set that now + self.natom = len(stuff) // 3 + self._coordinates = stuff.reshape((-1, self.natom, 3)) + self._file.write('%5d%15.7e\n' % (self.natom, self.time)) + numwrit = 0 + fmt = '%12.7f%12.7f%12.7f' + for i in range(self.natom): + i3 = i * 3 + self._file.write(fmt % (stuff[i3], stuff[i3+1], stuff[i3+2])) + numwrit += 1 + if numwrit % 2 == 0: + self._file.write('\n') + if self.natom % 2 == 1: + self._file.write('\n') + self._coords_written = True + + @property + def velocities(self): + if self._status == 'new' and not hasattr(self, '_velocities'): + raise RuntimeError('Velocities not set yet') + if not self.hasvels: + return None + return self._velocities + + @velocities.setter + def velocities(self, stuff): + if self._status == 'old': + raise RuntimeError('Cannot set velocities on an old restart') + stuff = np.array(stuff, copy=False).ravel() + if not self._coords_written: + raise RuntimeError('Coordinates must be set before velocities') + if self._cell_lengths_written or self._cell_angles_written: + raise RuntimeError('Velocities must be written before the box info') + if self._vels_written: + raise RuntimeError('Can only write velocities once') + if len(stuff) != 3 * self.natom: + raise ValueError(f'Got {len(stuff)} velocities for {self.natom} atoms.') + self._velocities = stuff.reshape((-1, self.natom, 3)) + fmt = '%12.7f%12.7f%12.7f' + numwrit = 0 + for i in range(self.natom): + i3 = i * 3 + self._file.write(fmt % tuple(stuff[i3+i]*ONEVELSCALE for i in range(3))) + numwrit += 1 + if numwrit % 2 == 0: + self._file.write('\n') + if self.natom % 2 == 1: + self._file.write('\n') + self._vels_written = self.hasvels = True + + @property + def cell_lengths(self): + if self._status == 'new' and not hasattr(self, '_cell_lengths'): + raise RuntimeError('Cell lengths not yet available') + if not self.hasbox: + return None + return self._cell_lengths + + @property + def cell_angles(self): + if self._status == 'new' and not hasattr(self, '_cell_angles'): + raise RuntimeError('Cell angles not yet available') + if not self.hasbox: + return None + return self._cell_angles + + @property + def box(self): + """ Combined cell lengths and cell angles """ + if self._status == 'new' and not (hasattr(self, '_cell_lengths') and + hasattr(self, '_cell_angles')): + raise RuntimeError('Cell parameters not yet set') + if not self.hasbox: + return None + box = np.zeros(6) + lengths, angles = self.cell_lengths, self.cell_angles + box[0:3] = [lengths[0], lengths[1], lengths[2]] + box[3:6] = [angles[0], angles[1], angles[2]] + return box + + @cell_lengths.setter + def cell_lengths(self, stuff): + if self._status == 'old': + raise RuntimeError('Cannot set cell lengths on old restart') + if not self._coords_written: + raise RuntimeError('Coordinates must be written before box') + if self._cell_lengths_written: + raise RuntimeError('Can only write cell lengths once') + if len(stuff) != 3: + raise ValueError('Expected 3 numbers for cell lengths') + self._cell_lengths = np.array(stuff, copy=False) + self._file.write('%12.7f%12.7f%12.7f' % (stuff[0], stuff[1], stuff[2])) + self._cell_lengths_written = True + + @cell_angles.setter + def cell_angles(self, stuff): + if self._status == 'old': + raise RuntimeError('Cannot set cell angles on old restart') + if not self._coords_written: + raise RuntimeError('Coordinates must be written before box') + if not self._cell_lengths_written: + raise RuntimeError('Must write cell lengths before angles') + if self._cell_angles_written: + raise RuntimeError('Can only write cell angles once') + if len(stuff) != 3: + raise ValueError('Expected 3 numbers for cell angles') + self._cell_angles = np.array(stuff, copy=False) + self._file.write('%12.7f%12.7f%12.7f\n' % (stuff[0],stuff[1],stuff[2])) + self._cell_angles_written = True + + @box.setter + def box(self, stuff): + """ Writes both the cell lengths and cell angles """ + self.cell_lengths = stuff[:3] + self.cell_angles = stuff[3:] + +#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +class AmberMdcrd(_AmberAsciiCoordinateFile): + """ + A class to parse Amber ASCII trajectory files. This is *much* slower than + parsing NetCDF files (or the equivalent parsing done in a compiled language + like C or C++). For large trajectories, this may be significant. + """ + extra_args = ('natom', 'hasbox') + + CRDS_PER_LINE = 10 + DEFAULT_TITLE = 'trajectory created by ParmEd' + + @staticmethod + def id_format(filename): + """ Identifies the file type as an Amber mdcrd file + + Parameters + ---------- + filename : str + Name of the file to check format for + + Returns + ------- + is_fmt : bool + True if it is an Amber mdcrd file. False otherwise + """ + f = genopen(filename, 'r') + lines = [f.readline() for i in range(5)] + f.close() + # Next 4 lines, make sure we have %8.3f format + try: + for i in range(4): + i += 1 + for j in range(10): + j8 = j * 8 + if lines[i][j8+4] != '.': return False + float(lines[i][j8:j8+8]) + if lines[i][j8+7] not in '0123456789': + return False + except (IndexError, ValueError): + return False + + # Must be a mdcrd + return True + + def _parse(self): + """ + This method parses the data out of the mdcrd file and creates + self._coordinates as a list of np.ndarray(1, natom, 3) and + self.cell_lengths as a list of np.ndarray(3) for each frame in the + trajectory. This method is called automatically for 'old' trajectory + files and should not be called by external callers. + """ + line = self._file.readline() + self.title = line.strip() + self._coordinates = np.ndarray(0) + self.cell_lengths = np.ndarray(0) + mainiter = range(0, 8*self.CRDS_PER_LINE, 8) + extraiter = range(0, 8*self._nextras, 8) + try: + while line: + frame = np.zeros(self.natom*3) + if not line: raise _FileEOF() + cell = np.zeros(3) + idx = 0 + line = self._file.readline() + if not line: raise StopIteration() + for i in range(self._full_lines_per_frame): + if not line: raise _FileEOF() + frame[idx:idx+10] = [float(line[j:j+8]) for j in mainiter] + idx += 10 + line = self._file.readline() + + if self._nextras: + frame[idx:idx+self._nextras] = [float(line[j:j+8]) for j in extraiter] + + if self.hasbox: + line = self._file.readline() + if not line: raise _FileEOF() + cell[0] = float(line[:8]) + cell[1] = float(line[8:16]) + cell[2] = float(line[16:24]) + + self._coordinates = np.concatenate((self._coordinates, frame)) + self.cell_lengths = np.concatenate((self.cell_lengths, cell)) + + except _FileEOF: + _warnings.warn('Unexpected EOF in parsing mdcrd. natom and/or hasbox are likely wrong', + RuntimeWarning) + except StopIteration: + pass + + self._coordinates = self._coordinates.reshape((-1, self.natom, 3)) + self.cell_lengths = self.cell_lengths.reshape((-1, 3)) + self._file.close() + + @property + def coordinates(self): + return self._coordinates + + @property + def frame(self): + return self._coordinates.shape[0] + + @property + def box(self): + if not self._status == 'old': + raise RuntimeError('Cannot access box of a new mdcrd') + return self.cell_lengths + + def add_coordinates(self, stuff): + """ + Prints 'stuff' (which must be either an iterable of 3*natom or have an + attribute 'flatten' that converts it into an iterable of 3*natom) to the + open file handler. Can only be called on a 'new' mdcrd, and adds these + coordinates to the current end of the file. + + Parameters + ---------- + stuff : array or iterable + This must be an iterable of length 3*natom or a numpy array that can + be flattened to a 3*natom-length array + + Raises + ------ + If the coordinate file is an old one being parsed or if you are + currently expected to provide unit cell dimensions, a RuntimeError is + raised. If the provided coordinate data does not have length 3*natom, or + cannot be ``flatten()``ed to create a 3*natom array, a ValueError is + raised. + """ + # Make sure we can write the coordinates right now + if not self._status == 'new': + raise RuntimeError('Cannot print frames to an old mdcrd') + try: + stuff = stuff.flatten() + except AttributeError: + pass + if self._writebox: + raise RuntimeError('Box information not written for last frame') + if len(stuff) != 3 * self.natom: + raise ValueError('add_coordinates requires an array of length natom*3') + + # If we can, write the coordinates + i = 0 + j = -1 + for i in range(self._full_lines_per_frame): + i10 = i * 10 + for j in range(10): + self._file.write('%8.3f' % stuff[i10+j]) + self._file.write('\n') + if self._nextras: + extra = i*10+j + 1 + while extra < self.natom*3: + self._file.write('%8.3f' % stuff[extra]) + extra += 1 + self._file.write('\n') + # Now it's time to write the box info if necessary + self._writebox = self.hasbox + self._file.flush() + + def add_box(self, stuff): + """ + Prints 'stuff' (which must be a 3-element list, array.array, tuple, or + np.ndarray) as the box lengths for this frame + + Parameters + ---------- + stuff : array or iterable + This must be an iterable of length 3 with the box lengths + + Raises + ------ + If the coordinate file is an old one being parsed or if you are + currently expected to provide coordinates, a RuntimeError is raised. + raised. If the provided box lengths are not length 3, a ValueError is + raised. + """ + # First make sure we should be writing our box now + if not self._status == 'new': + raise RuntimeError('Cannot print box to an old mdcrd') + if not self._writebox: + raise RuntimeError('Should not be writing box info right now') + if len(stuff) != 3: + raise ValueError('add_box requires an array of length 3') + + self._file.write('%8.3f%8.3f%8.3f\n' % (stuff[0], stuff[1], stuff[2])) + self._writebox = False + self._file.flush() + +#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ diff --git a/pmx/workflow/parmed/amber/mask.py b/pmx/workflow/parmed/amber/mask.py new file mode 100644 index 00000000..570237b2 --- /dev/null +++ b/pmx/workflow/parmed/amber/mask.py @@ -0,0 +1,732 @@ +""" +Module for evaluating Amber Mask strings and translating them into lists in +which a selected atom is 1 and one that's not is 0. +""" +from ..exceptions import MaskError +from ..periodic_table import AtomicNum + +#+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + +class AmberMask(object): + """ + What is hopefully a fully-fledged Amber mask parser implemented in Python. + + Parameters + ---------- + parm : Structure + The topology structure for which to select atoms + mask : str + The mask string that selects a subset of atoms + """ + + #====================================================== + + def __init__(self, parm, mask): + self.parm = parm + self.mask = mask.strip() + + #====================================================== + + def __str__(self): + return self.mask + + #====================================================== + + def Selected(self, invert=False): + """ Generator that returns the indexes of selected atoms + + Parameters + ---------- + invert : bool, optional + If True, all atoms *not* selected by the mask will be returned + + Returns + ------- + generator of int + Each iteration will yield the index of the next atom that has been + selected by the mask. Atom indices are 0-based + """ + for i, v in enumerate(self.Selection(invert=invert)): + if v: + yield i + + #====================================================== + + def Selection(self, prnlev=0, invert=False): + """ + Parses the mask and analyzes the result to return an atom + selection array + + Parameters + ---------- + prnlev : int, optional + Print debug information on the processing of the Amber mask string. + This is mainly useful if you are modifying the mask parser. Default + value is 0 (no printout), values between 1 and 8 control the level + of output (larger values produce more output). Default 0 + invert : bool, optional + If True, the returned array will invert the selection of the mask + (i.e., selected atoms will not be selected and vice-versa) + + Returns + ------- + mask : list of int + A list with length equal to the number of atoms in the assigned + :class:`Structure ` instance. Selected + atoms will have a value of 1 placed in the corresponding slot in the + return list while atoms not selected will be assigned 0. + """ + from sys import stderr, stdout + if prnlev > 2: stderr.write('In AmberMask.Selection(), debug active!\n') + if prnlev > 5: stdout.write('original mask: ==%s==\n' % self.mask) + + # 0) See if we got the default "all" mask(*) and return accordingly + if self.mask.strip() == '*': + return [1 for atom in self.parm.atoms] + + # 1) preprocess input expression + infix = self._tokenize(prnlev) + if prnlev > 5: stdout.write('tokenized mask: ==%s==\n' % infix) + + # 2) construct postfix (RPN) notation + postfix = self._torpn(infix, prnlev) + if prnlev > 5: stdout.write('postfix mask: ==%s==\n' % postfix) + + # 3) evaluate the postfix notation + if invert: + return [1-i for i in self._evaluate(postfix, prnlev)] + return self._evaluate(postfix, prnlev) + + #====================================================== + + def _tokenize(self, prnlev): + """ Tokenizes the mask string into individual selections: + 1. remove spaces + 2. isolate 'operands' into brackets [...] + 3. split expressions of the type :1-10@CA,CB into 2 parts; + the 2 parts are joned with & operator and (for the sake + of preserving precedence of other operators) enclosed by + (...); i.e. :1-10@CA,CB is split into (:1-10 & @CA,CB) + 4. do basic error checking + """ + buffer = '' # keeping track of a single operand + infix = '' # value that is returned at the end + # flag == 0: means new operand or operand was completed & ended with ] + # flag == 1: means operand with ":" read + # flag == 2: means operand with "@" read + # flag == 3: means '<' or '>' read, waiting for numbers + flag = 0 + i = 0 + while i < len(self.mask): + p = self.mask[i] + # skip whitespace + if p.isspace(): + i += 1 + continue + # If p is an operator, is the last character, or is a ()... + elif self._isOperator(p) or i == len(self.mask) - 1 or p in ['(',')']: + # Deal with the last character being a wildcard that we have to + # convert + if p == '=' and i == len(self.mask) - 1: # wildcard + if flag > 0: + p = '*' + else: + raise MaskError("AmberMask: '=' not in name list syntax") + # If this is the end of an operand, terminate the buffer, flush + # it to infix, and reset flag to 0 and empty the buffer + if flag > 0: + if i == len(self.mask) - 1 and p != ')': buffer += p + buffer += '])' + flag = 0 + infix += buffer + buffer = '' + if i != len(self.mask) - 1 or p == ')': infix += p + # else if p is >,< + if p in ['<','>']: + buffer = '([%s' % p + i += 1 + try: + p = self.mask[i] + except IndexError: + raise MaskError('Bad distance syntax [%s]' % self.mask) + buffer += p + flag = 3 + if self.parm.coordinates is None: + raise MaskError('<,> operators require coordinates') + if not p in [':','@']: + raise MaskError('Bad syntax [%s]' % self.mask) + elif self._isOperand(p): + if flag == 0: + buffer = '([' + flag = 1 + if p != '*': + raise MaskError('Bad syntax [%s]' % self.mask) + if p == '=': # wildcard + if flag > 0: + p = '*' + else: + raise MaskError("'=' not in name list syntax") + buffer += p + elif p == ':': + if flag == 0: + buffer = '([:' + flag = 1 + else: + buffer += '])|([:' + flag = 1 + elif p == '@': + if flag == 0: + buffer = '([@' + flag = 2 + elif flag == 1: + buffer += ']&[@' + flag = 2 + elif flag == 2: + buffer += '])|([@' + flag = 2 + else: + raise MaskError('Unknown symbol (%s) expression' % p) + i += 1 + # end while i < len(self.mask): + # Check that each operand has at least 4 characters: [:1] and [@C], etc. + i = 0 + n = 1 # number of characters in current operand + flag = 0 + while i < len(infix): + p = infix[i] + if p == '[': + n += 1 + flag = 1 + elif p == ']': + if n < 4 and infix[i-1] != '*': + raise MaskError('empty token in infix') + n = 1 + else: + if flag == 1: + n += 1 + i += 1 + + return infix + '\n' # terminating \n for next step + + #====================================================== + + def _isOperator(self, char): + """ Determines if a character is an operator """ + return len(char) == 1 and char in '!&|<>' + + #====================================================== + + def _isOperand(self, char): + """ Determines if a character is an operand """ + return len(char) == 1 and (char in "\\*/%-?,'.=+_" or char.isalnum()) + + #====================================================== + + def _torpn(self, infix, prnlev): + """ Converts the infix to an RPN array """ + postfix = '' + stack = ['\n'] # use a list as a stack. Then pop() works as expected + flag = 0 + i = 0 + + while i < len(infix): + p = infix[i] + if p == '[': + postfix += p + flag = 1 + elif p == ']': + postfix += p + flag = 0 + elif flag: + postfix += p + elif p == '(': + stack.append(p) + elif p == ')': + pp = stack.pop() + while pp != '(': + if pp == '\n': + raise MaskError('Unbalanced parentheses in Mask.') + postfix += pp + pp = stack.pop() + # At this point both ()s are discarded + elif p == '\n': + pp = stack.pop() + while pp != '\n': + if pp == '(': + raise MaskError('Unbalanced parentheses in Mask.') + postfix += pp + pp = stack.pop() + elif self._isOperator(p): + P1 = self._priority(p) + P2 = self._priority(stack[len(stack)-1]) + if P1 > P2: + stack.append(p) + else: + while P1 <= P2: + pp = stack.pop() + postfix += pp + P1 = self._priority(p) + P2 = self._priority(stack[len(stack)-1]) + stack.append(p) + else: + raise MaskError('Unknown symbol %s' % p) # should not reach here + i += 1 + # end while i < len(infix): + return postfix + + #====================================================== + + def _evaluate(self, postfix, prnlev): + """ Evaluates a postfix in RPN format and returns a selection array """ + from sys import stderr + buffer = '' + stack = [] + + pos = 0 # position in postfix + while pos < len(postfix): + p = postfix[pos] + if p == '[': buffer = '' + elif p == ']': # end of the token + ptoken = buffer + pmask = self._selectElemMask(ptoken) + stack.append(pmask) + elif self._isOperand(p) or p in [':','@']: + buffer += p + elif p in ['&','|']: + pmask1 = None + pmask2 = None + try: + pmask1 = stack.pop() + pmask2 = stack.pop() + pmask = self._binop(p, pmask1, pmask2) + except IndexError: + raise MaskError('Illegal binary operation') + stack.append(pmask) + elif p in ['<','>']: + if pos < len(postfix)-1 and postfix[pos+1] in [':','@']: + buffer += p + else: + try: + pmask1 = stack.pop() # distance criteria + pmask2 = stack.pop() + pmask = self._selectDistd(pmask1, pmask2) + except IndexError: + return [0 for a in self.parm.atoms] + stack.append(pmask) + elif p == '!': + try: + pmask1 = stack.pop() + except IndexError: + raise MaskError('Illegal ! operation') + pmask = self._neg(pmask1) + stack.append(pmask) + else: + raise MaskError('Unknown symbol evaluating RPN: %s' % p) + pos += 1 + # end while i < len(postfix) + + try: + pmask = stack.pop() + except IndexError: + raise MaskError('Empty stack -- no available operands') + + if stack: + raise MaskError('There may be missing operands in the mask') + + if prnlev > 7: + stderr.write('%d atoms selected by %s' % (sum(pmask), self.mask)) + + return pmask + + #====================================================== + + def _neg(self, pmask1): + """ Negates a given mask """ + return pmask1.Not() + + #====================================================== + + def _selectDistd(self, pmask1, pmask2): + """ Selects atoms based on a distance criteria """ + # pmask1 is either @ or :, and represents the distance + # criteria. pmask2 is the selection of atoms from which the distance is + # evaluated. + pmask = _mask(len(self.parm.atoms)) + # Determine if we want > or < + if pmask1[0] == '<': + cmp = lambda x, y: x < y + elif pmask1[0] == '>': + cmp = lambda x, y: x > y + else: # Should never execute this + raise MaskError('Unknown comparison criteria for distance mask: %s' % pmask1[0]) + pmask1 = pmask1[1:] + if pmask1[0] not in ':@': # Should never execute this + raise MaskError('Bad distance criteria for mask: %s' % pmask1) + try: + distance = float(pmask1[1:]) + except (TypeError, ValueError): + raise MaskError('Distance must be a number: %s' % pmask1[1:]) + distance *= distance # Faster to compare square of distance + # First select all atoms that satisfy the distance. If we ended up + # choosing residues, then we will go back through afterwards and select + # entire residues when one of the atoms in that residue is selected. + idxlist = [i for i, val in enumerate(pmask2) if val == 1] + for i, atomi in enumerate(self.parm.atoms): + for j in idxlist: + atomj = self.parm.atoms[j] + dx = atomi.xx - atomj.xx + dy = atomi.xy - atomj.xy + dz = atomi.xz - atomj.xz + d2 = dx*dx + dy*dy + dz*dz + if cmp(d2, distance): + pmask[i] = 1 + break + # Now see if we have to select all atoms in residues with any selected + # atoms + if pmask1[0] == ':': + for res in self.parm.residues: + for atom in res.atoms: + if pmask[atom.idx] == 1: + for atom in res.atoms: + pmask[atom.idx] = 1 + break + return pmask + + #====================================================== + + def _selectElemMask(self, ptoken): + """ Selects an element mask """ + # some constants + ALL = 0 + NUMLIST = 1 + NAMELIST = 2 + TYPELIST = 3 + ELEMLIST = 4 + # define the mask object and empty buffer + pmask = _mask(len(self.parm.atoms)) + buffer = '' + buffer_p = 0 + # This is a residue NUMber LIST + if ptoken.startswith(':'): + reslist = NUMLIST + pos = 1 + while pos < len(ptoken): + p = ptoken[pos] + buffer += p + buffer_p += 1 + if p == '*' and ptoken[pos-1] != '\\': + if buffer_p == 1 and (pos == len(ptoken) - 1 or ptoken[pos+1] == ','): + reslist = ALL + elif reslist == NUMLIST: + reslist = NAMELIST + elif p.isalpha() or p in '_?*': + reslist = NAMELIST + if pos == len(ptoken) - 1: + buffer_p = 0 + if len(buffer) != 0 and buffer_p == 0: + if reslist == ALL: + pmask.select_all() + elif reslist == NUMLIST: + self._residue_numlist(buffer, pmask) + elif reslist == NAMELIST: + self._residue_namelist(buffer, pmask) + reslist = NUMLIST + pos += 1 + elif ptoken.startswith('@'): + atomlist = NUMLIST + pos = 1 + while pos < len(ptoken): + p = ptoken[pos] + buffer += p + buffer_p += 1 + if p == '*' and ptoken[pos-1] != "\\": + if atomlist == NUMLIST: + atomlist = NAMELIST + elif p.isalpha() or p in '?*_': + if atomlist == NUMLIST: + atomlist = NAMELIST + elif p == '%': + atomlist = TYPELIST + elif p == '/': + atomlist = ELEMLIST + if pos == len(ptoken) - 1: + buffer_p = 0 + + if len(buffer) != 0 and buffer_p == 0: + if atomlist == ALL: + pmask.select_all() + elif atomlist == NUMLIST: + self._atom_numlist(buffer, pmask) + elif atomlist == NAMELIST: + self._atom_namelist(buffer, pmask) + elif atomlist == TYPELIST: + self._atom_typelist(buffer[1:], pmask) + elif atomlist == ELEMLIST: + self._atom_elemlist(buffer[1:], pmask) + pos += 1 + elif ptoken.strip() == '*': + pmask.select_all() + elif ptoken[0] in ['<','>']: + return ptoken + else: # Should never reach here + raise MaskError('Mask is missing : and @') + # end if ':' in ptoken: + + return pmask + + #====================================================== + + def _atom_numlist(self, instring, mask): + """ Fills a _mask based on atom numbers """ + buffer = '' + pos = 0 + at1 = at2 = dash = 0 + while pos < len(instring): + p = instring[pos] + if p.isdigit(): + buffer += p + if p == ',' or pos == len(instring) - 1: + if dash == 0: + at1 = int(buffer) + self._atnum_select(at1, at1, mask) + else: + at2 = int(buffer) + self._atnum_select(at1, at2, mask) + dash = 0 + buffer = '' + elif p == '-': + at1 = int(buffer) + dash = 1 + buffer = '' + if not (p.isdigit() or p in [',','-']): + raise MaskError('Unknown symbol in atom number parsing [%s]'%p) + pos += 1 + + #====================================================== + + def _atom_namelist(self, instring, mask, key='name'): + """ Fills a _mask based on atom names/types """ + buffer = '' + pos = 0 + while pos < len(instring): + p = instring[pos] + if p.isalnum() or p in "\\*?+'-_": + buffer += p + if p == ',' or pos == len(instring) - 1: + if '-' in buffer and buffer[0].isdigit(): + self._atom_numlist(buffer, mask) + else: + self._atname_select(buffer, mask, key) + buffer = '' + if not (p.isalnum() or p in "\\,?*'+-_"): + raise MaskError('Unrecognized symbol in atom name parsing [%s]' % p) + pos += 1 + + #====================================================== + + def _atom_typelist(self, buffer, mask): + """ Fills a _mask based on atom types """ + self._atom_namelist(buffer, mask, key='type') + + #====================================================== + + def _atom_elemlist(self, buffer, mask): + """ + Fills a _mask based on atom elements. For now it will just be Atom + names, since elements are not stored in the prmtop anywhere. + """ + self._atom_namelist(buffer, mask, key='element') + + #====================================================== + + def _residue_numlist(self, instring, mask): + """ Fills a _mask based on residue numbers """ + buffer = '' + pos = 0 + at1 = at2 = dash = 0 + while pos < len(instring): + p = instring[pos] + if p.isdigit(): + buffer += p + if p == ',' or pos == len(instring) - 1: + if dash == 0: + at1 = int(buffer) + self._resnum_select(at1, at1, mask) + else: + try: + at2 = int(buffer) + except ValueError: + raise MaskError('Bad mask: error in integer conversion') + self._resnum_select(at1, at2, mask) + dash = 0 + buffer = '' + elif p == '-': + at1 = int(buffer) + dash = 1 + buffer = '' + pos += 1 + + #====================================================== + + def _residue_namelist(self, instring, mask): + """ Fills a _mask based on residue names """ + buffer = '' + pos = 0 + while pos < len(instring): + p = instring[pos] + if p.isalnum() or p in ['*','?','+',"'",'-']: + buffer += p + if p == ',' or pos == len(instring) - 1: + if '-' in buffer and buffer[0].isdigit(): + self._residue_numlist(buffer, mask) + else: + self._resname_select(buffer, mask) + buffer = '' + if not (p.isalnum() or p in ",?*'+-"): + raise MaskError('Unknown symbol in residue name parsing [%s]' % p) + pos += 1 + + #====================================================== + + def _atnum_select(self, at1, at2, mask): + """ Fills a _mask array between atom numbers at1 and at2 """ + for i in range(at1-1, at2): mask[i] = 1 + + #====================================================== + + def _resnum_select(self, res1, res2, mask): + """ Fills a _mask array between residues res1 and res2 """ + for i, atom in enumerate(self.parm.atoms): + res = atom.residue.idx + 1 + if res >= res1 and res <= res2: mask[i] = 1 + + #====================================================== + + def _atname_select(self, atname, mask, key='name'): + """ Fills a _mask array with all atom names of a given name """ + if atname.isdigit(): + atname = int(atname) - 1 + for i, atom in enumerate(self.parm.atoms): + mask[i] = mask[i] | int(atname == i) + elif key == 'element': + try: + for i, atom in enumerate(self.parm.atoms): + mask[i] = mask[i] | int(AtomicNum[atname] == atom.atomic_number) + except KeyError: + raise MaskError('Unknown element %s' % atname) + else: + for i, atom in enumerate(self.parm.atoms): + mask[i] = mask[i] | int(_nameMatch(atname, getattr(atom, key))) + + #====================================================== + + def _resname_select(self, resname, mask): + """ Fills a _mask array with all residue names of a given name """ + for i, atm in enumerate(self.parm.atoms): + if _nameMatch(resname, atm.residue.name): + mask[i] = 1 + elif resname.isdigit(): + mask[i] = mask[i] | int(int(resname) == atm.residue.idx + 1) + + #====================================================== + + def _binop(self, op, pmask1, pmask2): + """ Does a binary operation on a pair of masks """ + if op == '&': + return pmask1.And(pmask2) + if op == '|': + return pmask1.Or(pmask2) + raise MaskError('Unknown operator [%s]' % op) + + #====================================================== + + def _priority(self, op): + if op in ['>','<']: return 6 + if op in ['!']: return 5 + if op in ['&']: return 4 + if op in ['|']: return 3 + if op in ['(']: return 2 + if op in ['\n']: return 1 + + raise MaskError('Unknown operator [%s] in Mask ==%s==' % (op, self.mask)) + +#+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + +def _nameMatch(atnam1, atnam2): + """ + Determines if atnam1 matches atnam2, where atnam1 can have * as a wildcard + and spaces are ignored. atnam2 should come from the prmtop. We'll use regex + to do this. + + We will replace * with a regex that will match any alphanumeric character + 0 or more times: * --> \\w* + + We will replace ? with a regex that will match exactly 1 alphanumeric + character: ? --> \\w + + Then, we will substitute all instances of atnam2 in atnam1 with ''. If it's + a complete match, then our result will be a blank string (and will evaluate + to False for boolean conditions). If it's not blank, then it's not a full + match and should return False + """ + import re + atnam1 = str(atnam1).replace(' ','') + atnam2 = str(atnam2).replace(' ','') + # Replace amber mask wildcards with appropriate regex wildcards and protect + # the + (but protect backslashes) + R = '' + atnam1 = atnam1.replace('\\*', R).replace('*',r'\S*').replace(R, '*') + atnam1 = atnam1.replace('\\?', R).replace('?',r'\S').replace(R, '?') + atnam1 = atnam1.replace('\\+', R).replace('+',r'\+').replace(R, '+') + # Now replace just the first instance of atnam2 in atnam2 with '', and + # return *not* that + return atnam1 == atnam2 or not bool(re.sub(atnam1, '', atnam2, 1)) + +#+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + +class _mask(list): + """ Mask array; only used by AmberMask """ + + def __init__(self, natom): + self.natom = natom + list.__init__(self, [0 for i in range(natom)]) + + def append(self, *args, **kwargs): + raise MaskError('_mask is a fixed-length array!') + + def extend(self, *args, **kwargs): + raise MaskError('_mask is a fixed-length array!') + + def pop(self, *args, **kwargs): + return self[-1] + + def remove(self, *args, **kwargs): + raise MaskError('_mask is a fixed-length array!') + + def And(self, other): + if self.natom != other.natom: + raise MaskError("_mask: and() requires another mask of equal size!") + new_mask = _mask(self.natom) + for i in range(len(self)): + new_mask[i] = int(self[i] and other[i]) + return new_mask + + def Or(self, other): + if self.natom != other.natom: + raise MaskError('_mask: or() requires another mask of equal size!') + new_mask = _mask(self.natom) + for i in range(len(self)): + new_mask[i] = int(self[i] or other[i]) + return new_mask + + def Not(self): + new_mask = _mask(self.natom) + for i in range(self.natom): + new_mask[i] = 1 - self[i] + return new_mask + + def select_all(self): + for i in range(self.natom): + self[i] = 1 + +#+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ diff --git a/pmx/workflow/parmed/amber/mdin/__init__.py b/pmx/workflow/parmed/amber/mdin/__init__.py new file mode 100644 index 00000000..e73f97b7 --- /dev/null +++ b/pmx/workflow/parmed/amber/mdin/__init__.py @@ -0,0 +1,4 @@ +""" Functionality for manipulating Amber input files """ +from .mdin import Mdin + +__all__ = ['Mdin'] diff --git a/pmx/workflow/parmed/amber/mdin/cntrl.py b/pmx/workflow/parmed/amber/mdin/cntrl.py new file mode 100644 index 00000000..9ab34e4c --- /dev/null +++ b/pmx/workflow/parmed/amber/mdin/cntrl.py @@ -0,0 +1,97 @@ +""" +This module contains all of the cntrl namelist variables for the +different amber programs and automatically loads those dictionaries +with the default values found in that program (sander or pmemd). +""" +from typing import Dict + +from .typing import InputDataType + +class cntrl: + + def __init__(self): + self.sander: Dict[str, InputDataType] = { + 'irest': 0, 'ibelly' : 0, 'ntx' : 1, 'ntxo' : 1, + 'ntcx' : 0, 'ig' : 71277, 'tempi' : 0.0, 'ntb' : -1, 'ntt' : 0, + 'nchain' : 1, 'temp0' : 300.0, 'tautp' : 1.0, 'ntp' : 0, + 'pres0' : 1.0, 'comp' : 44.6, 'taup' : 1.0, 'nscm' : 1000, + 'nstlim' : 1, 'dt' : 0.001, 'ntc' : 1, 'ntcc' : 0, 'nconp' : 0, + 'tol' : 0.00001, 'ntf' : 1, 'nsnb' : 25, 'cut' : 0.0, 'dielc' : 0, + 'ntpr' : 50, 'ntwx' : 0, 'ntwv' : 0, 'ntwe' : 0, 'ntave' : 0, + 'ntpp' : 0, 'ioutfm' : 0, 'ntr' : 0, 'nrc' : 0, 'ntrx' : 1, + 'taur' : 0, 'nmropt' : 0, 'ivcap' : 0, 'cutcap' : 0.0, + 'xcap' : 0.0, 'ycap' : 0.0, 'zcap' : 0.0, 'fcap' : 1.5, + 'xlorth' : -1.0, 'ylorth' : -1.0, 'zlorth' : -1.0, + 'xorth' : 47114711.0, 'yorth' : 47114711.0, 'zorth' : 47114711.0, + 'forth' : 1.5, 'imin' : 0, 'drms' : 1.0e-4, 'dele' : 0, + 'dx0' : 0.01, 'pencut' : 0.1, 'ipnlty' : 1, 'iscale' : 0, + 'scalm' : 100.0, 'noeskp' : 1, 'maxcyc' : 1, 'ncyc' : 10, + 'ntmin' : 1, 'vlimit' : 20.0, 'mxsub' : 1, 'ipol' : 0, + 'jfastw' : 0, 'watnam' : ' ', 'owtnm' : ' ', + 'hwtnm1' : ' ', 'hwtnm2' : ' ', 'iesp' : 0, 'skmin' : 50, + 'skmax' : 100, 'vv' : 0, 'vfac' : 0, 'tmode' : 1, 'ips' : 0, + 'mipsx' : -1, 'mipsy' : -1, 'mipsz' : -1, 'mipso' : 4, + 'gridips' : 2, 'raips' : -1.0, 'dvbips' : 1.0e-8, 'isgld' : 0, + 'isgsta' : 1, 'isgend' : 0, 'tsgavg' : 0.2, 'sgft' : 0.0, + 'tempsg' : 1.0, 'jar' : 0, 'iamoeba' : 0, 'numexchg' : 0, + 'repcrd' : 1, 'numwatkeep' : -1, 'hybridgb' : 0, 'ntwprt' : 0, + 'tausw' : 0.1, 'ntwr' : 500, 'iyammp' : 0, 'imcdo' : -1, + 'igb' : 0, 'alpb' : 0, 'arad' : 15.0, 'rgbmax' : 25.0, + 'saltcon' : 0.0, 'offset' : -999999.0, 'ntwf' : 0, + 'gbsa' : 0, 'vrand' : 1000, 'surften' : 0.005, 'iwrap' : 0, + 'nrespa' : 1, 'nrespai' : 1, 'gamma_ln' : 0.0, 'extdiel' : 78.5, + 'intdiel' : 1.0, 'cut_inner' : 8.0, 'icfe' : 0, 'clambda' : 0.0, + 'klambda' : 1, 'rbornstat' : 0, 'lastrst' : 1, 'lastist' : 1, + 'itgtmd' : 0, 'tgtrmsd' : 0, 'tgtmdfrc' : 0, 'tgtfitmask' : '', + 'tgtrmsmask' : '', 'idecomp' : 0, 'temp0les' : -1.0, + 'restraintmask' : '', 'restraint_wt' : 0.0, 'bellymask' : '', + 'noshakemask' : '', 'crgmask' : '', 'iwrap_mask' : '', + 'mmtsb_switch' : 0, 'mmtsb_iterations' : 100, 'rdt' : 0.0, + 'icnstph' : 0, 'solvph' : 7.0, 'ntcnstph' : 10, 'ifqnt' : 0, + 'ievb' : 0, 'ipimd' : 0, 'itimass' : 0, 'ineb' : 0, + 'profile_mpi' : 0, 'ilscivr' : 0, 'icorf_lsc' : 0, 'ipb' : 0, + 'inp' : 2, 'gbneckscale' : -999999.0, 'gbalphah' : 0.788440, + 'gbbetah' : 0.798699, 'gbgammah' : 0.437334, + 'gbalphac' : 0.733756, 'gbbetac' : 0.506378, + 'gbgammac' : 0.205844, 'gbalphan' : 0.503364, + 'gbbetan' : 0.316828, 'gbgamman' : 0.192915, + 'gbalphaos' : 0.867814, 'gbbetaos' : 0.876635, + 'gbgammaos' : 0.387882, 'gbalphap' : 1.0, 'gbbetap' : 0.8, + 'gbgammap' : 4.851, 'sh' : 1.425952, 'sc' : 1.058554, + 'sn' : 0.733599, 'so' : 1.061039, 'ss' : -0.703469, 'sp' : 0.5, + 't' : 0.0, 'ntn' : 0 , 'scalpha' : 0.5, 'scbeta' : 12.0, + 'ifsc' : 0, 'scmask' : '', 'logdvdl' : 0, 'dvdl_norest' : 0, + 'dynlmb' : 0, 'ifmbar' : 0, 'bar_intervall' : 100, + 'bar_l_min' : 0.1, 'bar_l_max' : 0.9, 'bar_l_incr' : 0.1, + 'idssp' : 0, 'irism' : 0, 'restart_cmd' : '.false.', + 'eq_cmd' : '.false.', 'adiab_param' : 1.0, 'dec_verbose' : 3, + 'mccycles' : 1, 'ifcr' : 0, 'iamd' : 0, 'barostat' : 1, + 'mcbarint' : 100, 'lj1264' : 0, + } + + self.pmemd: Dict[str, InputDataType] = { + 'imin' : 0, 'nmropt' : 0, 'ntx' : 1, 'irest' : 0, + 'ntrx' : 1, 'ntxo' : 1, 'ntpr' : 50, 'ntave' : 0, 'ntwr' : 500, + 'iwrap' : 0, 'ntwx' : 0, 'ntwv' : 0, 'ntwe' : 0, 'ioutfm' : 0, + 'ntwprt' : 0, 'ntf' : 1, 'ntb' : -1, 'dielc' : 1.0, 'cut' : 0.0, + 'nsnb' : 25, 'ipol' : 0, 'igb' : 0, 'intdiel' : 1.0, + 'extdiel' : 78.5, 'saltcon' : 0.0, 'rgbmax' : 25.0, + 'rbornstat' : 0, 'offset' : 0.09, 'gbsa' : 0, 'surften' : 0.005, + 'nrespai' : 1, 'cut_inner' : 8.0, 'ibelly' : 0, 'ntr' : 0, + 'bellymask' : 0, 'maxcyc' : 1, 'ncyc' : 10, 'ntmin' : 1, + 'dx0' : 0.01, 'drms' : 0.0001, 'nstlim' : 1, 'nscm' : 1000, + 't' : 0.0, 'dt' : 0.001, 'nrespa' : 1, 'ntt' : 0, 'temp0' : 300.0, + 'tempi' : 0.0, 'ig' : 71277, 'tautp' : 1.0, 'gamma_ln' : 0.0, + 'vrand' : 1000, 'vlimit' : 20.0, 'ntp' : 0, 'pres0' : 1.0, + 'comp' : 44.6, 'taup' : 1.0, 'ntc' : 1, 'tol' : 0.00001, + 'jfastw' : 0, 'hwtnm1' : ' ', 'hwtnm2' : ' ', + 'owtnm' : ' ', 'watnam' : ' ', 'ivcap' : 0, 'fcap' : 1.5, + 'pencut' : 0.1, 'ndfmin' : 0, 'jar' : 0, + 'no_intermolecular_bonds' : 1, 'ene_avg_sampling' : -1, + 'mdinfo_flush_interval' : 60, 'mdout_flush_interval' : 300, + 'dbg_atom_redistribution' : 0, 'loadbal_verbose' : 0, + 'es_cutoff' : 0.0, 'vdw_cutoff' : 0.0, 'ntwf' : 0, + 'dtemp' : 0, 'dxm' : 0, 'heat' : 0, 'alpb' : 0, 'arad' : 15.0, + 'iamd' : 0, 'restraintwt' : 0.0, 'barostat' : 1, 'mcbarint' : 100, + 'lj1264' : 0, 'icnstph' : 0, 'solvph' : 7.0, 'ntcnstph' : 10, + } diff --git a/pmx/workflow/parmed/amber/mdin/ewald.py b/pmx/workflow/parmed/amber/mdin/ewald.py new file mode 100644 index 00000000..3c9d84ac --- /dev/null +++ b/pmx/workflow/parmed/amber/mdin/ewald.py @@ -0,0 +1,35 @@ +""" +This module contains all of the ewald namelist variables for the +different amber programs and automatically loads those dictionaries +with the default values found in that program (sander or pmemd). +""" +from typing import Dict + +from .typing import InputDataType + +class ewald: + + def __init__(self): + self.sander: Dict[str, InputDataType] = { + 'dsum_tol' : 1.0e-5, 'ew_coeff' : 0.0, + 'skinnb' : 0.0, 'diptol' : 1.0e-4, 'dipmass' : 0.33, 'diptau' : 11.0, + 'nfft1' : 0, 'nfft2' : 0, 'nfft3' : 0, 'order' : 4, 'opt_infl' : 1, + 'verbose' : 0, 'nbflag' : 0, 'nbtell' : 0, 'netfrc' : 12344321, + 'ew_type' : 0, 'vdwmeth' : 1, 'eedmeth' : 1, 'ee_type' : 0, + 'eedtbdns' : 5000.0, 'rsum_tol' : 5.0e-5, 'maxexp' : 0.0, + 'mlimit' : "0,0,0", 'use_pme' : 0, 'maxiter' : 20, 'indmeth' : 3, 'irstdip' : 0, + 'nquench' : 0, 'frameon' : 1, 'chngmask' : 1, 'scaldip' : 1, + 'gridpointers' : 1, 'column_fft' : 1, + } + + self.pmemd: Dict[str, InputDataType] = { + 'nfft1' : 0, 'nfft2' : 0, 'nfft3' : 0, 'order' : 4, + 'verbose' : 0, 'ew_type' : 0, 'dsum_tol' : 1.0e-5, 'rsum_tol' : 5.0e-5, + 'mlimit' : "0,0,0", 'ew_coeff' : 0.0, 'nbflag' : 1, 'skinnb' : 2, + 'nbtell' : 0, 'netfrc' : -1, 'use_pme' : 1, 'vdwmeth' : 1, + 'eedmeth' : 1, 'eedtbdns' : 5000.0, 'ee_type' : 0, 'frameon' : 1, + 'chngmask' : 1, 'alpha' : 0.0, 'beta' : 0.0, 'gamma' : 0.0, + 'a' : 0.0, 'b' : 0.0, 'c' : 0.0, 'use_axis_opt' : -1, + 'fft_grids_per_ang' : 1.0, 'block_fft' : -1, 'fft_blk_y_divisor' : -1, + 'excl_recip' : -1, 'excl_master' : -1, 'atm_redist_freq' : -1, + } diff --git a/pmx/workflow/parmed/amber/mdin/mdin.py b/pmx/workflow/parmed/amber/mdin/mdin.py new file mode 100644 index 00000000..880a0062 --- /dev/null +++ b/pmx/workflow/parmed/amber/mdin/mdin.py @@ -0,0 +1,304 @@ +""" +This module will create an amber mdin file for either sander or +pmemd (or others). The program specification loads the appropriate +dictionaries with default values, etc. It can read and write mdins. +""" +from io import TextIOBase + +# This module will create and read a sander/pmemd input +from .cntrl import cntrl +from .ewald import ewald +from .pb import pb +from .qmmm import qmmm +from ...exceptions import InputError + + +def addOn(line, string, file): + if len(line.strip()) == 0: + return line + string + elif len(line) + len(string) > 40: + file.write(line + '\n') + return ' ' + string + else: + return line + string + + +class Mdin: + + def __init__(self, program = 'sander', verbosity = 1): + # define instance data + self.program = program # which program we're creating the input file for + self.cntrl_obj = cntrl() # object with cntrl namelist vars in a dictionary + self.ewald_obj = ewald() # object with ewald namelist vars in a dictionary + self.pb_obj = pb() # object with pb namelist vars in a dictionary + self.qmmm_obj = qmmm() # object with qmmm namelist vars in a dictionary + self.verbosity = 0 # verbosity level: 0 -- print nothing + # 1 -- print errors + # 2 -- 1 + warnings + # 3 -- 2 + notes + self.cards = [] # array that has all of the input cards that come + # after namelists + self.cntrl_nml = {} # dictionary with cntrl namelist vars + self.cntrl_nml_defaults = {} # dictionary with default cntrl namelist vars + self.ewald_nml = {} # dictionary with ewald namelist vars + self.ewald_nml_defaults = {} # dictionary with default ewald namelist vars + self.pb_nml = {} # dictionary with pb namelist vars + self.pb_nml_defaults = {} # dictionary with default pb namelist vars + self.qmmm_nml = {} # dictionary with qmmm namelist vars + self.qmmm_nml_defaults = {} # dictionary with default qmmm namelist vars + self.valid_namelists = {} # array with valid namelists for each program + self.title = 'mdin prepared by ParmEd' # title for the mdin file + + + if self.program == "sander": + self.cntrl_nml = self.cntrl_obj.sander + self.ewald_nml = self.ewald_obj.sander + self.pb_nml = self.pb_obj.sander + self.qmmm_nml = self.qmmm_obj.sander + self.valid_namelists = {'cntrl', 'ewald', 'qmmm', 'pb'} + elif self.program == "sander.APBS": + self.cntrl_nml = self.cntrl_obj.sander + self.pb_nml = self.pb_obj.sanderAPBS + self.valid_namelists = {'cntrl', 'apbs'} + elif self.program == "pmemd": + self.cntrl_nml = self.cntrl_obj.pmemd + self.ewald_nml = self.ewald_obj.pmemd + self.valid_namelists = {'cntrl', 'ewald'} + else: + raise InputError(f"Unrecognized program [{self.program}]") + + self.cntrl_nml_defaults = self.cntrl_nml.copy() + self.ewald_nml_defaults = self.ewald_nml.copy() + self.pb_nml_defaults = self.pb_nml.copy() + self.qmmm_nml_defaults = self.qmmm_nml.copy() + + def write(self, filename: str = 'mdin'): + + def write_nml(nml, defaults, file: TextIOBase, header) -> None: + # automatic indent of single space + line = ' ' + header_printed = False + # add any variable that is different from the default to the mdin file + for var, val in nml.items(): + if val == defaults[var]: + continue + if not header_printed: + file.write(f"{header}\n") + header_printed = True + if isinstance(val, str): + line = addOn(line, f"{var}='{val}', ", file) + else: + line = addOn(line, f"{var}={val}, ", file) + # flush any remaining items that haven't yet been printed to the mdin file + if line.strip(): + file.write(f"{line}\n") + # End namelist + if header_printed: + file.write('/\n') + + # open the file for writing and write the header and &cntrl namelist + file = open(filename,'w') + file.write(self.title + '\n') + write_nml(self.cntrl_nml, self.cntrl_nml_defaults, file, "&cntrl") + write_nml(self.ewald_nml, self.ewald_nml_defaults, file, "&ewald") + pb_nml_name = "&apbs" if self.program == "sander.APBS" else "&pb" + write_nml(self.pb_nml, self.pb_nml_defaults, file, pb_nml_name) + if self.cntrl_nml["ifqnt"] == 1: + write_nml(self.qmmm_nml, self.qmmm_nml_defaults, file, "&qmmm") + + # Write the cards to the input file + file.write("\n".join([card.strip() for card in self.cards])) + if len(self.cards) != 0: + file.write('\nEND\n') + + file.close() + + def read(self, filename = 'mdin'): + lines = open(filename, 'r').readlines() + + # split up input file into separate fields by comma + blocks = [] # namelists in the order they appear + block_fields = [] # array of arrays that correspond to entries in + # namelists found in "blocks" above + inblock = False + lead_comment = True + for i in range(len(lines)): + if not inblock and not lines[i].strip().startswith('&') and lead_comment: + continue + elif not inblock and not lines[i].strip().startswith('&') and not lead_comment: + final_ended = True + for j in range(i,len(lines)): + if lines[j].strip().startswith('&'): + final_ended = False + if final_ended and len(lines[i].strip()) != 0: + self.cards.append(lines[i]) + elif not inblock and lines[i].strip().startswith('&'): + lead_comment = False + inblock = True + block = lines[i].strip()[1:].lower() + blocks.append(block) # add the name of the namelist to "blocks" + block_fields.append([]) # add empty array to be filled with entries + # for given namelist + if not block in self.valid_namelists: + raise InputError(f'Invalid namelist ({lines[i].strip()}) in input file ({filename}) for {self.program}') + elif inblock and (lines[i].strip() == '/' or lines[i].strip() == '&end'): + inblock = False + elif inblock and lines[i].strip().startswith('&'): + raise InputError(f"Invalid input file ({filename}). Namelist not terminated") + elif inblock: + items = lines[i].strip().split(',') + j = 0 + while j < len(items): + items[j] = items[j].strip() + if len(items[j]) == 0: + items.pop(j) + else: + j += 1 + block_fields[len(block_fields)-1].extend(items) + + # take out the last END in the cards if it's there + if len(self.cards) != 0 and self.cards[len(self.cards)-1].strip().upper() == 'END': + self.cards.pop() + + # combine any multi-element fields: e.g. rstwt=1,2,3, + begin_field = -1 + for i in range(len(block_fields)): + for j in range(len(block_fields[i])): + if not '=' in block_fields[i][j]: + if begin_field == -1: + raise InputError(f'Invalid input file ({filename}).') + else: + block_fields[i][begin_field] += ',' + block_fields[i][j] + else: + begin_field = j + + # now parse through the options and add them to the dictionaries + for i in range(len(block_fields)): + for j in range(len(block_fields[i])): + if not '=' in block_fields[i][j]: + continue + else: + var = block_fields[i][j].split('=') + self.change(blocks[i], var[0].strip(), var[1].strip()) + + def change(self, namelist, variable, value): + """ Change the value of a variable without adding a new key-pair """ + + namelist_map = dict( + cntrl=(self.cntrl_nml, self.cntrl_nml_defaults), + ewald=(self.ewald_nml, self.ewald_nml_defaults), + pb=(self.pb_nml, self.pb_nml_defaults), + apbs=(self.pb_nml, self.pb_nml_defaults), + qmmm=(self.qmmm_nml, self.qmmm_nml_defaults), + ) + + variable = variable.lower() + if isinstance(value, str): + if (value[0] == value[-1] == "'") or (value[0] == value[-1] == '"'): + value = value[1:-1] + + if namelist in namelist_map: + nml, nml_defaults = namelist_map[namelist] + if variable in nml: + mytype = type(nml_defaults[variable]) + nml[variable] = mytype(value) + else: + raise InputError(f"Unknown variable {variable} in &{namelist}") + else: + raise InputError(f'Unknown namelist ({namelist})!') + + def check(self): + return True + + def SHAKE(self): + self.change('cntrl','ntf', 2) + self.change('cntrl','ntc', 2) + self.change('cntrl','dt', 0.002) + + def constPressure(self, press=1.0, taup=1.0): + self.change('cntrl','ntb', 2) + self.change('cntrl','ntp', 1) + self.change('cntrl','pres0', press) + self.change('cntrl','taup', taup) + + def constVolume(self): + self.change('cntrl','ntb', 1) + self.change('cntrl','ntp', 0) + + def constTemp(self, ntt=3, temp=300.0, gamma_ln=2.0, ig=-1, tautp=1.0): + self.change('cntrl','ntt', ntt) + self.change('cntrl','temp0', temp) + self.change('cntrl','tempi', temp) + self.change('cntrl','gamma_ln', gamma_ln if ntt==3 else 0) + self.change('cntrl','ig', ig) + self.change('cntrl','tautp', tautp) + + def constpH(self, solvph=7.0, igb=2, ntcnstph=10): + self.change('cntrl','icnstph', 1) + self.change('cntrl','solvph', solvph) + self.change('cntrl','ntcnstph', ntcnstph) + self.change('cntrl','igb', igb) + self.change('cntrl','ntb', 0) + self.change('cntrl','saltcon', 0.1) + + def restrainHeavyAtoms(self, restraint_wt=0.0): + self.change('cntrl','ntr', 1) + self.change('cntrl','restraint_wt', restraint_wt) + self.change('cntrl','restraintmask', '!@H=') + + def restrainBackbone(self, restraint_wt=0.0): + self.change('cntrl','ntr', 1) + self.change('cntrl','restraint_wt', restraint_wt) + self.change('cntrl','restraintmask', '@N,CA,C') + + def genBorn(self, igb=5, rgbmax=25.0): + self.change('cntrl','igb', igb) + self.change('cntrl','ntb', 0) + self.change('cntrl','ntp', 0) + self.change('cntrl','rgbmax', rgbmax) + + def time(self, time=1000.0, dt=None): # time in ps + if dt is None: + if self.cntrl_nml['ntc'] == 2 and self.cntrl_nml['ntf'] == 2: + dt = 0.002 + else: + dt = 0.001 + time = int(time / dt) + + self.change('cntrl','dt', dt) + self.change('cntrl','nstlim', time) + self.change('cntrl','imin', 0) + + def heat(self, tempi=0.0, temp0=300.0, ntt=3, tautp=5.0, ig=-1, gamma_ln=5.0): + self.constVolume() + self.change('cntrl','tempi', tempi) + self.change('cntrl','temp0', temp0) + self.change('cntrl','ntt', ntt) + self.change('cntrl','tautp', tautp) + self.change('cntrl','ig', ig) + self.change('cntrl','gamma_ln', gamma_ln if ntt==3 else 0) + + def restart(self,ntx=5): + self.change('cntrl','irest',1) + self.change('cntrl','ntx',ntx) + + def TI(self, clambda=0.0): + self.change('cntrl','clambda', clambda) + self.change('cntrl','icfe',1) + + def softcore_TI(self, scalpha=0.5, scmask='', crgmask='', logdvdl=0): + self.change('cntrl','icfe',1) + self.change('cntrl','ifsc',1) + self.change('cntrl','scalpha',scalpha) + self.change('cntrl','scmask',scmask) + self.change('cntrl','crgmask',crgmask) + self.change('cntrl','logdvdl',logdvdl) + + def minimization(self, imin=1, maxcyc=1, ncyc=10, ntmin=1): + self.change('cntrl','imin', imin) + self.change('cntrl','maxcyc', maxcyc) + self.change('cntrl','ncyc', ncyc) + self.change('cntrl','ntmin', ntmin) + + def AddCard(self, title='Residues in card', cardString='RES 1'): + self.cards.append('%s\n%s\nEND' % (title, cardString)) diff --git a/pmx/workflow/parmed/amber/mdin/pb.py b/pmx/workflow/parmed/amber/mdin/pb.py new file mode 100644 index 00000000..5ab5bf10 --- /dev/null +++ b/pmx/workflow/parmed/amber/mdin/pb.py @@ -0,0 +1,54 @@ +""" +This module contains all of the pb namelist variables for the amber +programs and automatically loads those dictionaries with the default +values found in that program (sander or sander.APBS) +""" +from typing import Dict + +from .typing import InputDataType + +class pb: + + def __init__(self): + self.sander: Dict[str, InputDataType] = { + 'epsin' : 1.0, 'epsout' : 80.0, 'smoothopt' : 1, + 'istrng' : 0.0, 'pbtemp' : 300.0, 'radiopt' : 1, + 'dprob' : 1.4, 'iprob' : 2.0, 'npbopt' : 0, 'solvopt' : 1, + 'accept' : 0.001, 'maxitn' : 100, 'fillratio' : 2.0, + 'space' : 0.5, 'nbuffer' : 0, 'nfocus' : 2, 'fscale' : 8, + 'npbgrid' : 1, 'arcres' : 0.25, 'bcopt' : 5, + 'scalec' : 0, 'eneopt' : 2, 'frcopt' : 0, 'cutfd' : 5.0, + 'cutnb' : 0.0, 'nsnbr' : 1, 'nsnba' : 1, 'phiout' : 0, + 'phiform' : 0, 'npbverb' : 0, 'npopt' : 2, 'decompopt' : 2, + 'use_rmin' : 1, 'sprob' : 0.557, 'vprob' : 1.3, + 'rhow_effect' : 1.129, 'use_sav' : 1, + 'cavity_surften' : 0.0378, 'cavity_offset' : -0.5692, + 'maxsph' : 400, 'maxarc' : 512, 'cutsa' : 9.0, 'ndofd' : 1, + 'ndosas' : 1, 'fmiccg' : 0.9, 'ivalence' : 1.0, + 'laccept' : 0.1, 'wsor' : 1.9, 'lwsor' : 1.95, + 'pbkappa' : 0, 'radinc' : 0.8, 'expthresh' : 0.2, + 'offx' : 0.0, 'offy' : 0.0, 'offz' : 0.0, + 'sepbuf' : 4.0, 'mpopt' : 0, 'lmax' : 80, 'maxarcdot' : 1500, + 'epsmem': 1.0, 'membraneopt' : 0, 'sasopt' : 0, 'mthick' : 40.0, + 'mctrdz' : 0.0, 'mprob' : 2.7, 'poretype': 1, 'npbverb' : 0, + } + + self. sanderAPBS: Dict[str, InputDataType] = { + 'dime' : '0,0,0', 'pdime' : '0,0,0', 'cglen' : '0,0,0', + 'fglen' : '0,0,0', 'grid' : '0,0,0', 'nlev' : 4, + 'nonlin' : 0, 'bcfl' : 1, 'nion' : 0, 'pdie' : 2.0, + 'sdie' : 78.4, 'srfm' : 2, 'chgm' : 1, 'temp' : 298.15, + 'gamma' : 0.105, 'sdens' : 10.0, 'calc_type' : 1, + 'calcnpenergy' : 1, 'cmeth' : 1, 'ccmeth' : 1, + 'fcmeth' : 1, 'ionq' : 'NO_DEFAULT', + 'ionc' : 'NO_DEFAULT', 'ionrr' : 'NO_DEFAULT', + 'smvolume' : 10, 'smsize' : 1000.0, 'srad' : 1.4, + 'swin' : 0.3, 'calcenergy' : 2, 'calcforce' : 2, + 'apbs_debug' : 0, 'sp_apbs' : '.false.', + 'apbs_print' : 1, 'wpot' : 0, 'wchg' : 0, 'wsmol' : 0, + 'ispara' : 0, 'radiopt' : 0, 'geom_upd_limit' : 0, + 'evdw_upd_limit' : 0, 'pqr' : 0, 'dime_updates' : 0, + 'wkappa' : 0, 'wdiel' : 0, 'rchg' : 0, 'rkappa' : 0, + 'rdiel' : 0, 'calcnpforce' : 2, 'watompot' : 0, + 'rpot' : 0, + } diff --git a/pmx/workflow/parmed/amber/mdin/qmmm.py b/pmx/workflow/parmed/amber/mdin/qmmm.py new file mode 100644 index 00000000..6f3819a8 --- /dev/null +++ b/pmx/workflow/parmed/amber/mdin/qmmm.py @@ -0,0 +1,33 @@ +""" +This module contains all of the qmmm namelist variables for the +amber programs and automatically loads those dictionaries with the +default values found in that program (sander only). +""" +from typing import Dict + +from .typing import InputDataType +class qmmm: + + def __init__(self): + self.sander: Dict[str, InputDataType] = { + 'qmcut' : -1, 'iqmatoms' : '', 'qmmask' : '', 'qmgb' : 2, + 'qm_theory' : '', 'qmcharge' : 0, 'qmqmdx' : 1, + 'verbosity' : 0, 'tight_p_conv' : 0, 'scfconv' : 1.0e-8, + 'errconv' : 1e-1, 'ndiis_matrices' :6, 'ndiis_attempts' : 0, + 'printcharges' : 0, 'printdipole' : 0, 'peptide_corr' : 0, + 'itrmax' : 1000, 'qmshake' : 1, 'qmqm_erep_incore' : 1, + 'qmmmrij_incore' : 1, 'lnk_dis' : 1.09, 'lnk_atomic_no' : 1, + 'lnk_method' : 1, 'spin' : 1, 'pseudo_diag' : 1, + 'pseudo_diag_criteria' : 0.05, 'qm_ewald' : 1, 'qm_pme' : 1, + 'kmaxqx' : 5, 'kmaxqy' : 5, 'kmaxqz' : 5, 'ksqmaxq' : 27, + 'writepdb' : 0, 'qmmm_int' : 1, 'adjust_q' : 2, + 'diag_routine' : 1, 'density_predict' : 0, + 'fock_predict' : 0, 'fockp_d1' : 2.4, 'fockp_d2' : -1.2, + 'fockp_d3' : -0.8, 'fockp_d4' : 0.6, 'idc' : 0, 'divpb' : 0, + 'dftb_maxiter' : 70, 'dftb_disper' : 0, + 'dftb_3rd_order' : 'NONE', 'dftb_chg' : 0, + 'dftb_telec' : 0, 'dftb_telec_step' : 0, + 'qmmm_omp_max_threads' : 1, 'chg_lambda' : 1, + 'nearest_qm_solvent' : 0, 'nearest_qm_solvent_fq' : 1, + 'nearest_qm_solvent_resname' : 'WAT ', + } diff --git a/pmx/workflow/parmed/amber/mdin/typing.py b/pmx/workflow/parmed/amber/mdin/typing.py new file mode 100644 index 00000000..0550b581 --- /dev/null +++ b/pmx/workflow/parmed/amber/mdin/typing.py @@ -0,0 +1,4 @@ +""" Utilities to aid with type hinting """ +from typing import Union + +InputDataType = Union[int, float, str] diff --git a/pmx/workflow/parmed/amber/netcdffiles.py b/pmx/workflow/parmed/amber/netcdffiles.py new file mode 100644 index 00000000..4554ff67 --- /dev/null +++ b/pmx/workflow/parmed/amber/netcdffiles.py @@ -0,0 +1,766 @@ +""" +This module contains classes for reading and writing Amber NetCDF-style files, +including both restarts and trajectories. The NetCDF engine used here is pulled +from the scipy distribution, and depends *only* on numpy. It is available +through parmed.utils.netcdf. + +This module contains objects relevant to Amber NetCDF files. The use() function +is responsible for selecting the API based on a default choice or user-selection +(the latter is really only helpful for development to ensure that all packages +work correctly---there is no difference from a user perspective). ALL +NetCDF-file manipulation that the parmed/amber package does should be +contained in this module. +""" +try: + # netCDF4 is *much* faster to write NetCDF files, and can make a huge + # difference when using it as an OpenMM reporter. So do what we can to use + # the faster library when available + import netCDF4 as nc +except ImportError: + nc = None +import numpy as np +from .. import __version__ +from ..formats.registry import FileFormatType +from .. import unit as u +from ..utils.netcdf import netcdf_file as NetCDFFile +import warnings + +class NetCDFRestart(metaclass=FileFormatType): + """ Class to read or write NetCDF restart files """ + + @staticmethod + def id_format(filename): + """ Identifies the file type as an Amber NetCDF restart file + + Parameters + ---------- + filename : str + Name of the file to check format for + + Returns + ------- + is_fmt : bool + True if it is an Amber NetCDF restart file. False otherwise + + Notes + ----- + Remote NetCDF files cannot be loaded + """ + if filename.startswith('http://') or filename.startswith('https://')\ + or filename.startswith('ftp://'): + return False + try: + f = NetCDFFile(filename, 'r', mmap=False) + except (TypeError, OSError): + return False + try: + try: + if f.Conventions.decode() != 'AMBERRESTART': + return False + except AttributeError: + return False + # Passed all our tests + return True + finally: + f.close() + + def __init__(self, fname, mode='r'): + """ + Opens a NetCDF File. The main constructor should never be called + directly. The alternative "open_old" and "open_new" constructors should + be called instead for reading existing or writing new NetCDF files, + respectively. + """ + self.closed = False + if mode.startswith('w') and nc is not None: + self._ncfile = nc.Dataset(fname, mode, format='NETCDF3_64BIT') + else: + if mode.startswith('w'): + warnings.warn('Could not find netCDF4 module. Falling back on ' + 'scipy implementation, which can significantly ' + 'slow down simulations if used as a reporter') + self._ncfile = NetCDFFile(fname, mode, mmap=False) + + @classmethod + def open_new(cls, fname, natom, box, vels, title='', + remd=None, temp=None, remd_dimtypes=None): + """ + Opens a new NetCDF file and sets the attributes + + Parameters + ---------- + fname : str + Name of the new file to open (overwritten) + natom : int + The number of atoms in the system + box : bool + Whether unit cell information is written or not + vels : bool + Whether velocity information is written or not + title : str='' + The title to write to the NetCDF restart file + remd : str=None + None -- No REMD information is written + 'T[emperature]' -- target temperature (or pH) will be written + 'M[ulti-D]' -- remd_dimtypes will be written + remd_dimtypes : iterable of int=None + Array of exchange types for each group. The length will be the REMD + dimension (if `remd` above is "M[ulti-D]") + + Notes + ----- + `remd` is case-insensitive, and done based on first-letter matching + """ + if remd is not None: + if remd[0] in 'tT': + remd_type = 'TEMPERATURE' + if temp is None: + raise ValueError('temp must be specified for T-REMD restarts.') + elif remd[0] in 'mM': + remd_type = 'MULTI' + if remd_dimtypes is None: + raise ValueError('remd_dimtypes must be given for multi-D ' + 'REMD, and must have the same length.') + for dt in remd_dimtypes: + if dt not in (1, 3): + raise ValueError( + 'remd_dimtypes only supports dimension types 1 and 3 currently' + ) + remd_dimension = len(remd_dimtypes) + else: + raise ValueError('remd must be None, T[emperature] or M[ulti]') + else: + remd_type = None + inst = cls(fname, 'w') + ncfile = inst._ncfile + inst.hasbox = bool(box) + inst.hasvels = bool(vels) + # Assign the main attributes + ncfile.Conventions = 'AMBERRESTART' + ncfile.ConventionVersion = "1.0" + ncfile.title = str(title) # Cast to avoid ScientificPython segfault + ncfile.application = "AmberTools" + ncfile.program = "ParmEd" + ncfile.programVersion = str(__version__) + # Make all of the dimensions + ncfile.createDimension('spatial', 3) + ncfile.createDimension('atom', natom) + inst.spatial = 3 + inst.atom = natom + inst.title = ncfile.title + if box: + ncfile.createDimension('cell_spatial', 3) + ncfile.createDimension('label', 5) + ncfile.createDimension('cell_angular', 3) + inst.cell_spatial = 3 + inst.label = 5 + inst.cell_angular = 3 + if remd_type == 'MULTI': + ncfile.createDimension('remd_dimension', remd_dimension) + inst.remd_dimension = remd_dimension + ncfile.createDimension('time', 1) + # Now make the variables + v = ncfile.createVariable('time', 'd', ('time',)) + v.units = 'picosecond' + v = ncfile.createVariable('spatial', 'c', ('spatial',)) + v[:] = np.asarray(list('xyz')) + if inst.hasbox: + v = ncfile.createVariable('cell_angular', 'c', ('cell_angular', 'label')) + v[0] = np.asarray(list('alpha')) + v[1] = np.asarray(list('beta ')) + v[2] = np.asarray(list('gamma')) + v = ncfile.createVariable('cell_spatial', 'c', ('cell_spatial',)) + v[0], v[1], v[2] = 'a', 'b', 'c' + v = ncfile.createVariable('cell_lengths', 'd', ('cell_spatial',)) + v.units = 'angstrom' + v = ncfile.createVariable('cell_angles', 'd', ('cell_angular',)) + v.units = 'degree' + v = ncfile.createVariable('coordinates', 'd', ('atom', 'spatial')) + v.units = 'angstrom' + if inst.hasvels: + v = ncfile.createVariable('velocities', 'd', ('atom', 'spatial')) + v.units = 'angstrom/picosecond' + v.scale_factor = np.float32(20.455) + inst.velocity_scale = 20.455 + if nc is not None: + v.set_auto_maskandscale(False) + + if remd_type == 'TEMPERATURE': + v = ncfile.createVariable('temp0', 'd', ('time',)) + v.units = 'kelvin' + v[0] = temp + elif remd_type == 'MULTI': + ncfile.createVariable('remd_indices', 'i', ('remd_dimension',)) + v = ncfile.createVariable('remd_dimtype', 'i', ('remd_dimension',)) + v[:] = remd_dimtypes + + return inst + + @classmethod + def open_old(cls, fname): + """ + Opens the NetCDF file and sets the global attributes that the file sets + + Parameters + ---------- + fname : str + Name of the file to read + """ + inst = cls(fname, 'r') + ncfile = inst._ncfile + inst.Conventions = ncfile.Conventions.decode() + inst.ConventionVersion = ncfile.ConventionVersion.decode() + inst.program = ncfile.program.decode() + inst.programVersion = ncfile.programVersion.decode() + if hasattr(ncfile, 'application'): + inst.application = ncfile.application.decode() + else: + inst.application = None + if hasattr(ncfile, 'title'): + inst.title = ncfile.title.decode() + else: + inst.title = None + # Set up the dimensions as attributes + for dim in ncfile.dimensions: + # Exception for ParmEd-created ncrst files + if dim == 'time': continue + setattr(inst, dim, ncfile.dimensions[dim]) + inst.hasvels = 'velocities' in ncfile.variables + inst.hasbox = ('cell_lengths' in ncfile.variables and 'cell_angles' in ncfile.variables) + if inst.hasvels: + vels = ncfile.variables['velocities'] + inst.velocity_scale = vels.scale_factor + return inst + + parse = open_old + + @property + def coordinates(self): + coords = self._ncfile.variables['coordinates'][:] + return coords.reshape((-1, self.atom, 3)) + + @coordinates.setter + def coordinates(self, stuff): + stuff = np.array(stuff, copy=False).reshape((self.atom, 3)) + self._ncfile.variables['coordinates'][:] = stuff + self.flush() + + @property + def velocities(self): + if 'velocities' in self._ncfile.variables: + vels = self._ncfile.variables['velocities'][:] + return (vels.reshape((-1, self.atom, 3)) * self.velocity_scale) + + @velocities.setter + def velocities(self, stuff): + self._ncfile.variables['velocities'][:] = \ + np.reshape(stuff, (self.atom, 3)) / self.velocity_scale + self.flush() + + @property + def cell_lengths(self): + if 'cell_lengths' in self._ncfile.variables: + return self._ncfile.variables['cell_lengths'][:] + + @cell_lengths.setter + def cell_lengths(self, stuff): + self._ncfile.variables['cell_lengths'][:] = np.asarray(stuff) + self.flush() + + @property + def cell_angles(self): + if 'cell_angles' in self._ncfile.variables: + return self._ncfile.variables['cell_angles'][:] + + @cell_angles.setter + def cell_angles(self, stuff): + self._ncfile.variables['cell_angles'][:] = np.asarray(stuff) + self.flush() + + @property + def box(self): + if self.cell_lengths is not None and self.cell_angles is not None: + leng, ang = self.cell_lengths, self.cell_angles + return np.concatenate((leng, ang)) + + @box.setter + def box(self, stuff): + self.cell_lengths = stuff[:3] + self.cell_angles = stuff[3:] + + @property + def time(self): + return self._ncfile.variables['time'].getValue() + + @time.setter + def time(self, stuff): + self._ncfile.variables['time'][0] = float(stuff) + self.flush() + + @property + def temp0(self): + return self._ncfile.variables['temp0'].getValue() + + @property + def remd_indices(self): + return self._ncfile.variables['remd_indices'][:] + + @remd_indices.setter + def remd_indices(self, stuff): + self._ncfile.variables['remd_indices'][:] = np.asarray(stuff, dtype='i') + self.flush() + + @property + def remd_dimtype(self): + return self._ncfile.variables['remd_dimtype'][:] + + def close(self): + if not self.closed: + self.closed = True + self._ncfile.close() + + def __del__(self): + self.closed or (hasattr(self, '_ncfile') and self._ncfile.close()) + + def flush(self): + try: + self._ncfile.flush() + except AttributeError: + # netCDF4.Dataset's flush method is called sync :-P + self._ncfile.sync() + +class NetCDFTraj(metaclass=FileFormatType): + """ Class to read or write NetCDF restart files + + Parameters + ---------- + fname : str + Name of the file to open + mode : str + Mode to open in: + - 'w' means write-mode + - 'r' means read-mode + + Notes + ----- + You should use the open_new and open_old alternative constructors instead of + the default constructor + """ + + @staticmethod + def id_format(filename): + """ Identifies the file type as an Amber NetCDF trajectory file + + Parameters + ---------- + filename : str + Name of the file to check format for + + Returns + ------- + is_fmt : bool + True if it is an Amber NetCDF trajectory file. False otherwise + + Notes + ----- + Remote NetCDF files cannot be loaded + """ + if filename.startswith('http://') or filename.startswith('https://')\ + or filename.startswith('ftp://'): + return False + try: + f = NetCDFFile(filename, 'r', mmap=False) + except (TypeError, OSError): + return False + try: + try: + if f.Conventions.decode() != 'AMBER': + return False + except AttributeError: + return False + # Passed all our tests + return True + finally: + f.close() + + def __init__(self, fname, mode='r'): + """ Opens a NetCDF File """ + self.closed = False + if mode.startswith('w') and nc is not None: + self._ncfile = nc.Dataset(fname, mode, format='NETCDF3_64BIT') + else: + if mode.startswith('w'): + warnings.warn('Could not find netCDF4 module. Falling back on ' + 'scipy implementation, which can significantly ' + 'slow down simulations if used as a reporter') + self._ncfile = NetCDFFile(fname, mode, mmap=False) + + @classmethod + def open_new(cls, fname, natom, box, crds=True, vels=False, frcs=False, + remd=None, remd_dimension=None, title=''): + """ + Opens a new NetCDF file and sets the attributes + + Parameters + ---------- + fname : str + Name of the new file to open (overwritten) + natom : int + Number of atoms in the restart + box : bool + Indicates if cell lengths and angles are written to the NetCDF file + crds : bool=True + Indicates if coordinates are written to the NetCDF file + vels : bool=False + Indicates if velocities are written to the NetCDF file + frcs : bool=False + Indicates if forces are written to the NetCDF file + remd : str=None + 'T[emperature]' if replica temperature is written + 'M[ulti]' if Multi-D REMD information is written + None if no REMD information is written + remd_dimension : int=None + If remd above is 'M[ulti]', this is how many REMD dimensions exist + title : str='' + The title of the NetCDF trajectory file + """ + inst = cls(fname, 'w') + ncfile = inst._ncfile + if remd is not None: + if remd[0] in 'Tt': + inst.remd = 'TEMPERATURE' + elif remd[0] in 'Mm': + inst.remd = 'MULTI' + if remd_dimension is None: + raise ValueError('remd_dimension must be given for multi-D REMD') + inst.remd_dimension = int(remd_dimension) + else: + raise ValueError('remd must be T[emperature] or M[ultiD]') + else: + inst.remd = None + inst.hasbox = bool(box) + inst.hasvels = bool(vels) + inst.hascrds = bool(crds) + inst.hasfrcs = bool(frcs) + # Assign the main attributes + ncfile.Conventions = "AMBER" + ncfile.ConventionVersion = "1.0" + ncfile.application = "AmberTools" + ncfile.program = "ParmEd" + ncfile.programVersion = __version__ + ncfile.title = "ParmEd-created trajectory" + inst.Conventions = "AMBER" + inst.ConventionVersion = "1.0" + inst.application = "AmberTools" + inst.program = "ParmEd" + inst.programVersion = __version__ + inst.title = ncfile.title + # Create the dimensions + ncfile.createDimension('frame', None) + ncfile.createDimension('spatial', 3) + ncfile.createDimension('atom', natom) + if inst.remd == 'MULTI': + ncfile.createDimension('remd_dimension', inst.remd_dimension) + inst.frame, inst.spatial, inst.atom = None, 3, natom + if inst.hasbox: + ncfile.createDimension('cell_spatial', 3) + ncfile.createDimension('cell_angular', 3) + ncfile.createDimension('label', 5) + inst.cell_spatial, inst.cell_angular, inst.label = 3, 3, 5 + # Create the variables and assign units and scaling factors + v = ncfile.createVariable('spatial', 'c', ('spatial',)) + v[:] = np.asarray(list('xyz')) + if inst.hasbox: + v = ncfile.createVariable('cell_spatial', 'c', ('cell_spatial',)) + v[:] = np.asarray(list('abc')) + v = ncfile.createVariable('cell_angular', 'c', ('cell_angular', 'label',)) + v[:] = np.asarray([list('alpha'), list('beta '), list('gamma')]) + v = ncfile.createVariable('time', 'f', ('frame',)) + v.units = 'picosecond' + if inst.hascrds: + v = ncfile.createVariable('coordinates', 'f', ('frame', 'atom', 'spatial')) + v.units = 'angstrom' + inst._last_crd_frame = 0 + if inst.hasvels: + v = ncfile.createVariable('velocities', 'f', ('frame', 'atom', 'spatial')) + v.units = 'angstrom/picosecond' + inst.velocity_scale = v.scale_factor = 20.455 + inst._last_vel_frame = 0 + if nc is not None: + v.set_auto_maskandscale(False) + if inst.hasfrcs: + v = ncfile.createVariable('forces', 'f', ('frame', 'atom', 'spatial')) + v.units = 'kilocalorie/mole/angstrom' + inst._last_frc_frame = 0 + if inst.hasbox: + v = ncfile.createVariable('cell_lengths', 'd', ('frame', 'cell_spatial')) + v.units = 'angstrom' + v = ncfile.createVariable('cell_angles', 'd', ('frame', 'cell_angular')) + v.units = 'degree' + inst._last_box_frame = 0 + if inst.remd == 'TEMPERATURE': + v = ncfile.createVariable('temp0', 'd', ('frame',)) + v.units = 'kelvin' + inst._last_remd_frame = 0 + elif inst.remd == 'MULTI': + ncfile.createVariable('remd_indices', 'i', ('frame', 'remd_dimension')) + ncfile.createVariable('remd_dimtype', 'i', ('remd_dimension',)) + inst._last_remd_frame = 0 + + inst._last_time_frame = 0 + + return inst + + @classmethod + def open_old(cls, fname): + """ + Opens the NetCDF file and sets the global attributes that the file sets + + Parameters + ---------- + fname : str + File name of the trajectory to open. It must exist + """ + inst = cls(fname, 'r') + ncfile = inst._ncfile + inst.Conventions = ncfile.Conventions.decode() + inst.ConventionVersion = ncfile.ConventionVersion.decode() + inst.program = ncfile.program.decode() + inst.programVersion = ncfile.programVersion.decode() + if hasattr(ncfile, 'application'): + inst.application = ncfile.application.decode() + else: + inst.application = None + if hasattr(ncfile, 'title'): + inst.title = ncfile.title.decode() + else: + inst.title = None + # Set up the dimensions as attributes + for dim in ncfile.dimensions: + setattr(inst, dim, ncfile.dimensions[dim]) + inst.hascrds = 'coordinates' in ncfile.variables + inst.hasvels = 'velocities' in ncfile.variables + inst.hasfrcs = 'forces' in ncfile.variables + inst.hasbox = ('cell_lengths' in ncfile.variables and 'cell_angles' in ncfile.variables) + if inst.hascrds: + inst._coordinates = np.array(ncfile.variables['coordinates'][:]) + if inst.hasvels: + try: + scale = ncfile.variables['velocities'].scale_factor + except AttributeError: + scale = 1 + inst._velocities = np.array(ncfile.variables['velocities'][:])*scale + inst.velocity_scale = scale + if inst.hasfrcs: + inst._forces = np.array(ncfile.variables['forces'][:]) + if inst.frame is None: + if 'time' in ncfile.variables: + inst.frame = len(ncfile.variables['time'][:]) + elif inst.hascrds: + inst.frame = inst._coordinates.shape[0] + elif inst.hasvels: + inst.frame = inst._velocities.shape[0] + elif inst.hasfrcs: + inst.frame = inst._forces.shape[0] + return inst + + @property + def coordinates(self): + return self._coordinates + + def add_coordinates(self, stuff): + """ + Adds a new coordinate frame to the end of a NetCDF trajectory. This + should only be called on objects created with the "open_new" + constructor. + + Parameters + ---------- + stuff : iterable of floats or distance Quantity + This array of floats is converted into a numpy array of shape + (natom, 3). It can be passed either in the 2-D format of + [ [x1, y1, z1], [x2, y2, z2], ... ] or in the 1-D format of + [x1, y1, z1, x2, y2, z2, ... ]. + """ + if u.is_quantity(stuff): + stuff = stuff.value_in_unit(u.angstroms) + stuff = np.asarray(stuff, dtype='f') + self._ncfile.variables['coordinates'][self._last_crd_frame] = \ + np.reshape(stuff, (self.atom, 3)) + self._last_crd_frame += 1 + self.flush() + + @property + def velocities(self): + return self._velocities + + def add_velocities(self, stuff): + """ + Adds a new velocities frame to the end of a NetCDF trajectory. This + should only be called on objects created with the "open_new" + constructor. + + Parameters + ---------- + stuff : iterable of floats or distance/time Quantity + This array of floats is converted into a numpy array of shape + (natom, 3). It can be passed either in the 2-D format of + [ [x1, y1, z1], [x2, y2, z2], ... ] or in the 1-D format of + [x1, y1, z1, x2, y2, z2, ... ]. + """ + if u.is_quantity(stuff): + stuff = stuff.value_in_unit(u.angstrom/u.picosecond) + stuff = np.asarray(stuff) + self._ncfile.variables['velocities'][self._last_vel_frame] = \ + np.reshape(stuff, (self.atom, 3)) / self.velocity_scale + self._last_vel_frame += 1 + self.flush() + + @property + def forces(self): + return self._forces + + def add_forces(self, stuff): + """ + Adds a new coordinate frame to the end of a NetCDF trajectory. This + should only be called on objects created with the "open_new" + constructor. + + Parameters + ---------- + stuff : iterable of floats or energy/distance Quantity + This array of floats is converted into a numpy array of shape + (natom, 3). It can be passed either in the 2-D format of + [ [x1, y1, z1], [x2, y2, z2], ... ] or in the 1-D format of + [x1, y1, z1, x2, y2, z2, ... ]. + """ + if u.is_quantity(stuff): + stuff.value_in_unit(u.kilocalories_per_mole/u.angstroms) + self._ncfile.variables['forces'][self._last_frc_frame] = \ + np.reshape(stuff, (self.atom, 3)) + self._last_frc_frame += 1 + self.flush() + + @property + def cell_lengths_angles(self): + try: + return np.hstack( + ( + self._ncfile.variables['cell_lengths'][:], + self._ncfile.variables['cell_angles'][:], + ) + ) + except KeyError: + return None + + box = cell_lengths_angles + + def add_cell_lengths_angles(self, lengths, angles=None): + """ + Adds a new cell length and angle frame to the end of a NetCDF + trajectory. This should only be called on objects created with the + "open_new" constructor. + + Parameters + ---------- + lengths : array of 3 (or 6) floats (or Quantities) + This should be a 1-D array of 3 or 6 elements. If 6 elements, + `angles` should be None (below) and the first 3 elements are the box + lengths (angstroms) and the last 3 are the box angles (degrees). + angles : 3-item iterable = None + These are the box angles (if lengths contains only 3 elements) in + degrees. Must be a 1-D array of 3 elements or None if lengths + includes angles as well. + """ + def strip_units(x, desired_units): + if u.is_quantity(x): return x.value_in_unit(desired_units) + return x + if len(lengths) == 3 and angles is None: + raise ValueError('Both lengths and angles are required.') + if len(lengths) == 6 and angles is not None: + raise ValueError('Angles can be provided only once.') + if len(lengths) != 6 and (len(lengths) != 3 or len(angles) != 3): + raise ValueError('6 numbers expected -- 3 lengths and 3 angles.') + if angles is None: + angles = [strip_units(x, u.degrees) for x in lengths[3:]] + lengths = [strip_units(x, u.angstroms) for x in lengths[:3]] + self._ncfile.variables['cell_lengths'][self._last_box_frame] = np.asarray(lengths) + self._ncfile.variables['cell_angles'][self._last_box_frame] = np.asarray(angles) + self._last_box_frame += 1 + self.flush() + + add_box = add_cell_lengths_angles + + @property + def time(self): + return self._ncfile.variables['time'][:] + + def add_time(self, stuff): + """ Adds the time to the current frame of the NetCDF file + + Parameters + ---------- + stuff : float or time-dimension Quantity + The time to add to the current frame + """ + if u.is_quantity(stuff): stuff = stuff.value_in_unit(u.picoseconds) + self._ncfile.variables['time'][self._last_time_frame] = float(stuff) + self._last_time_frame += 1 + self.flush() + + @property + def remd_indices(self): + return self._ncfile.variables['remd_indices'][:] + + def add_remd_indices(self, stuff): + """ Add REMD indices to the current frame of the NetCDF file + + Parameters + ---------- + stuff : iterable of int + The indices in each REMD dimension + """ + self._ncfile.variables['remd_indices'][self._last_remd_frame] = \ + np.asarray(stuff, dtype='i') + self._last_remd_frame += 1 + self.flush() + + @property + def temp0(self): + return self._ncfile.variables['temp0'][:] + + def add_temp0(self, stuff): + """ The temperature to add to the current frame of the NetCDF file + + Parameters + ---------- + stuff : float or temperature Quantity + The temperature to add to the current NetCDF file + """ + if u.is_quantity(stuff): stuff = stuff.value_in_unit(u.kelvin) + self._ncfile.variables['temp0'][self._last_remd_frame] = float(stuff) + self._last_remd_frame += 1 + self.flush() + + @property + def remd_dimtype(self): + return self._ncfile.variables['remd_dimtype'][:] + + @remd_dimtype.setter + def remd_dimtype(self, stuff): + self._ncfile.variables['remd_dimtype'][:] = np.asarray(stuff, dtype='i') + self.flush() + + def close(self): + """ Closes the NetCDF file """ + if not self.closed: + self._ncfile.close() + self.closed = True + + def __del__(self): + self.closed or (hasattr(self, '_ncfile') and self._ncfile.close()) + + def flush(self): + if nc is None: + # netCDF4.Dataset does not have a flush method + self._ncfile.flush() diff --git a/pmx/workflow/parmed/amber/offlib.py b/pmx/workflow/parmed/amber/offlib.py new file mode 100644 index 00000000..0602b9f3 --- /dev/null +++ b/pmx/workflow/parmed/amber/offlib.py @@ -0,0 +1,630 @@ +""" +Tool for parsing and writing OFF library files to and from dictionaries of +ResidueTemplate objects +""" +from collections import OrderedDict +from contextlib import closing +import numpy as np +from ..topologyobjects import Atom +from ..constants import RAD_TO_DEG +from ..exceptions import AmberWarning +from ..formats.registry import FileFormatType +from ..modeller.residue import ResidueTemplate, ResidueTemplateContainer +from ..modeller.residue import PROTEIN, NUCLEIC, SOLVENT, UNKNOWN +from .. import periodic_table as pt +from ..utils.io import genopen +import re +import warnings + +class AmberOFFLibrary(metaclass=FileFormatType): + """ + Class containing static methods responsible for parsing and writing OFF + libraries + """ + #=================================================== + + # Useful regexes + _headerre = re.compile(r'!!index *array *str') + _resre = re.compile(r'\s*"(\S*?)"\s*$') + _sec1re = re.compile(r'!entry\.(\S*?)\.unit\.atoms *table *str *name *str' + r' *type *int *typex *int *resx *int *flags *int' + r' *seq *int *elmnt *dbl *chg') + _sec2re = re.compile(r'!entry\.(\S*?)\.unit\.atomspertinfo *table *str' + r' *pname *str *ptype *int *ptypex *int *pelmnt' + r' *dbl *pchg') + _sec3re = re.compile(r'!entry\.(\S*?)\.unit\.boundbox *array *dbl') + _sec4re = re.compile(r'!entry\.(\S*?)\.unit\.childsequence *single *int') + _sec5re = re.compile(r'!entry\.(\S*?)\.unit\.connect *array *int') + _sec6re = re.compile(r'!entry\.(\S*?)\.unit\.connectivity *table *int' + r' *atom1x *int *atom2x *int *flags') + _sec7re = re.compile(r'!entry\.(\S*?)\.unit\.hierarchy *table *str' + r' *abovetype *int *abovex *str *belowtype *int' + r' *belowx') + _sec8re = re.compile(r'!entry\.(\S*?)\.unit\.name *single *str') + _sec9re = re.compile(r'!entry\.(\S*?)\.unit\.positions *table *dbl *x' + r' *dbl *y *dbl *z') + _sec10re = re.compile(r'!entry\.(\S*?)\.unit\.residueconnect *table' + r' *int *c1x *int *c2x *int *c3x *int *c4x *int' + r' *c5x *int *c6x') + _sec11re = re.compile(r'!entry\.(\S*?)\.unit\.residues *table *str *name' + r' *int *seq *int *childseq *int *startatomx *str' + r' *restype *int *imagingx') + _sec12re = re.compile(r'!entry\.(\S*?)\.unit\.residuesPdbSequenceNumber' + r' *array *int') + _sec13re = re.compile(r'!entry\.(\S*?)\.unit\.solventcap *array *dbl') + _sec14re = re.compile(r'!entry\.(\S*?)\.unit\.velocities *table *dbl *x' + r' *dbl *y *dbl *z') + + #=================================================== + + @classmethod + def id_format(cls, filename): + """ Sees if an open file is an OFF library file. + + Parameters + ---------- + filename : str + The name of the file to see if it is an OFF file format + + Returns + ------- + is_fmt : bool + True if it is recognized as OFF, False otherwise + """ + with closing(genopen(filename, 'r')) as f: + if cls._headerre.match(f.readline()): + return True + return False + + #=================================================== + + @classmethod + def parse(cls, filename): + """ Parses an Amber OFF library + + Parameters + ---------- + filename : str or file-like iterable + The file name or file object to parse. If it is an iterable, it will + be exhausted + + Returns + ------- + residues : OrderedDict {str : :class:`ResidueTemplate`} + Dictionary pairing residue names with their :class:`ResidueTemplate` + objects + + Raises + ------ + ValueError if the first line does not match the file format. This line + will be consumed + + IOError if filename is the name of a file that does not exist + + RuntimeError if EOF is reached prematurely or other formatting issues + found + """ + if isinstance(filename, str): + fileobj = genopen(filename, 'r') + own_handle = True + else: + fileobj = filename + own_handle = False + # Now parse the library file + line = fileobj.readline() + if not cls._headerre.match(line): + raise ValueError('Unrecognized OFF file format') + # Build the return value + residues = OrderedDict() + # Pull a list of all the residues we expect to find + line = fileobj.readline() + rematch = cls._resre.match(line) + while rematch and line: + name = rematch.groups()[0] + residues[name] = None + line = fileobj.readline() + rematch = cls._resre.match(line) + if not line: + raise RuntimeError('Unexpected EOF in Amber OFF library') + # Now make sure we have the next expected line + while line: + if not line.strip(): + line = fileobj.readline() + continue + rematch = cls._sec1re.match(line) + if not rematch: + raise RuntimeError('Expected atoms table not found') + name = rematch.groups()[0] + residues[name] = cls._parse_residue(fileobj, name) + line = fileobj.readline() + + if own_handle: + fileobj.close() + + return residues + + #=================================================== + + @classmethod + def _parse_residue(cls, fileobj, name): + """ + Parses the residue information out of the OFF file assuming the file + is pointed at the first line of an atoms table section of the OFF file + + Parameters + ---------- + fileobj : file-like + Assumed to be open for read, this file is parsed until the *next* + atom table is read + name : str + The name of the residue being processed right now + """ + container = ResidueTemplateContainer(name) + nres = 1 + templ = ResidueTemplate(name) + line = fileobj.readline() + while line[0] != '!': + nam, typ, typx, resx, flags, seq, elmnt, chg = line.split() + nam = _strip_enveloping_quotes(nam) + typ = _strip_enveloping_quotes(typ) + typx = int(typx) + resx = int(resx) + flags = int(flags) + seq = int(seq) + elmnt = int(elmnt) + chg = float(chg) + atom = Atom(atomic_number=elmnt, type=typ, name=nam, charge=chg) + if resx == nres + 1: + container.append(templ) + nres += 1 + templ = ResidueTemplate(name) + templ.add_atom(atom) + line = fileobj.readline() + # Skip blank lines + while line and not line.strip(): + line = fileobj.readline() + container.append(templ) + if nres > 1: + start_atoms = [] + runsum = 0 + for res in container: + start_atoms.append(runsum) + runsum += len(res) + # Make sure we get the next section + rematch = cls._sec2re.match(line) + if not rematch: + raise RuntimeError('Expected pertinfo table not found') + elif rematch.groups()[0] != name: + raise RuntimeError( + f'Found residue {rematch.groups()[0]} while processing residue {name}' + ) + line = fileobj.readline() + while line[0] != '!': + if not line: + raise RuntimeError('Unexpected EOF in Amber OFF library') + # Not used, just skip + # TODO sanity check + line = fileobj.readline() + rematch = cls._sec3re.match(line) + if not rematch: + raise RuntimeError('Expected boundbox table not found') + elif rematch.groups()[0] != name: + raise RuntimeError( + f'Found residue {rematch.groups()[0]} while processing residue {name}' + ) + # Only 5 lines + try: + hasbox = float(fileobj.readline().strip()) + angle = float(fileobj.readline().strip()) + a = float(fileobj.readline().strip()) + b = float(fileobj.readline().strip()) + c = float(fileobj.readline().strip()) + except ValueError: + raise RuntimeError('Error processing boundbox table entries') + else: + if hasbox > 0: + if angle < 3.15: + # No box is this acute -- must be in radians + angle *= RAD_TO_DEG + container.box = [a, b, c, angle, angle, angle] + # Get the child sequence entry + line = fileobj.readline() + rematch = cls._sec4re.match(line) + if not rematch: + raise RuntimeError('Expected childsequence table not found') + elif rematch.groups()[0] != name: + raise RuntimeError( + f"Found residue {rematch.groups()[0]} while processing residue {name}" + ) + n = int(fileobj.readline().strip()) + if nres + 1 != n: + warnings.warn( + f"Unexpected childsequence ({n}); expected {nres+1} for residue {name}", + AmberWarning, + ) + elif not isinstance(templ, ResidueTemplate) and n != len(templ) + 1: + raise RuntimeError("child sequence must be # of residues in the unit + 1") + # Get the CONNECT array to set head and tail + line = fileobj.readline() + rematch = cls._sec5re.match(line) + if not rematch: + raise RuntimeError('Expected connect array not found') + elif rematch.groups()[0] != name: + raise RuntimeError( + f"Found residue {rematch.groups()[0]} while processing residue {name}" + ) + try: + head = int(fileobj.readline().strip()) + tail = int(fileobj.readline().strip()) + except ValueError: + raise RuntimeError('Error processing connect table entries') + if head > 0 and nres == 1: + templ.head = templ[head-1] + elif head > 0 and nres > 1: + if head < sum((len(r) for r in container)): + raise RuntimeError('HEAD on multi-residue unit not supported') + if tail > 0 and nres == 1: + templ.tail = templ[tail-1] + elif tail > 0 and nres > 1: + if tail < sum((len(r) for r in container)): + warnings.warn( + f'TAIL on multi-residue unit not supported ({name}). Ignored...', AmberWarning + ) + # Get the connectivity array to set bonds + line = fileobj.readline() + if len(templ.atoms) > 1: + rematch = cls._sec6re.match(line) + if not rematch: + raise RuntimeError('Expected connectivity table not found') + elif rematch.groups()[0] != name: + raise RuntimeError( + f"Found residue {rematch.groups()[0]} while processing residue {name}" + ) + line = fileobj.readline() + while line[0] != '!': + i, j, flag = line.split() + line = fileobj.readline() + if nres > 1: + # Find which residue we belong in + i = int(i) - 1 + j = int(j) - 1 + for ii, idx in enumerate(start_atoms): + if idx > i: + ii -= 1 + break + start_idx = start_atoms[ii] + container[ii].add_bond(i-start_idx, j-start_idx) + else: + templ.add_bond(int(i)-1, int(j)-1) + # Get the hierarchy table + rematch = cls._sec7re.match(line) + if not rematch: + raise RuntimeError('Expected hierarchy table not found') + elif rematch.groups()[0] != name: + raise RuntimeError( + f"Found residue {rematch.groups()[0]} while processing residue {name}" + ) + line = fileobj.readline() + while line[0] != '!': + # Skip this section... not used + # TODO turn this into a sanity check + line = fileobj.readline() + # Get the unit name + rematch = cls._sec8re.match(line) + if not rematch: + raise RuntimeError('Expected unit name string not found') + elif rematch.groups()[0] != name: + raise RuntimeError( + f"Found residue {rematch.groups()[0]} while processing residue {name}" + ) + fileobj.readline() # Skip this... not used + line = fileobj.readline() + # Get the atomic positions + rematch = cls._sec9re.match(line) + if not rematch: + raise RuntimeError('Expected unit positions table not found') + elif rematch.groups()[0] != name: + raise RuntimeError( + f"Found residue {rematch.groups()[0]} while processing residue {name}" + ) + for res in container: + for atom in res: + x, y, z = fileobj.readline().split() + atom.xx, atom.xy, atom.xz = float(x), float(y), float(z) + line = fileobj.readline() + # Get the residueconnect table + rematch = cls._sec10re.match(line) + if not rematch: + raise RuntimeError('Expected unit residueconnect table not found') + elif rematch.groups()[0] != name: + raise RuntimeError( + f"Found residue {rematch.groups()[0]} while processing residue {name}" + ) + for i in range(nres): + c1,c2,c3,c4,c5,c6 = (int(x) for x in fileobj.readline().split()) + if (c1 > 0 and templ.head is not None and + templ.head is not templ[c1-1]): + raise RuntimeError('HEAD atom is not connect0') + if (c2 > 0 and templ.tail is not None and + templ.tail is not templ[c2-1]): + raise RuntimeError('TAIL atom is not connect1') + for i in (c3, c4, c5, c6): + if i == 0: continue + templ.connections.append(templ[i-1]) + # Get the residues table + line = fileobj.readline() + rematch = cls._sec11re.match(line) + if not rematch: + raise RuntimeError('Expected unit residues table not found') + elif rematch.groups()[0] != name: + raise RuntimeError( + f"Found residue {rematch.groups()[0]} while processing residue {name}" + ) + for i in range(nres): + resname, id, next, start, typ, img = fileobj.readline().split() + resname = _strip_enveloping_quotes(resname) + id = int(id) + start = int(start) + next = int(next) + typ = _strip_enveloping_quotes(typ) + img = int(img) + if next - start != len(container[i]): + warnings.warn( + f'residue table predicted {next - start}, not {len(container[i])} atoms for residue {name}', + AmberWarning, + ) + if typ == 'p': + container[i].type = PROTEIN + elif typ == 'n': + container[i].type = NUCLEIC + elif typ == 'w': + container[i].type = SOLVENT + elif typ != '?': + warnings.warn('Unknown residue type "%s"' % typ, AmberWarning) + if nres > 1: + container[i].name = resname + # Get the residues sequence table + line = fileobj.readline() + rematch = cls._sec12re.match(line) + if not rematch: + raise RuntimeError('Expected residue sequence number not found') + elif rematch.groups()[0] != name: + raise RuntimeError( + f"Found residue {rematch.groups()[0]} while processing residue {name}" + ) + for i in range(nres): + #TODO sanity check + fileobj.readline() + line = fileobj.readline() + # Get the solventcap array + rematch = cls._sec13re.match(line) + if not rematch: + raise RuntimeError('Expected unit solventcap array not found') + elif rematch.groups()[0] != name: + raise RuntimeError( + f"Found residue {rematch.groups()[0]} while processing residue {name}" + ) + # Ignore the solvent cap + fileobj.readline() + fileobj.readline() + fileobj.readline() + fileobj.readline() + fileobj.readline() + # Velocities + line = fileobj.readline() + rematch = cls._sec14re.match(line) + if not rematch: + raise RuntimeError('Expected unit solventcap array not found') + elif rematch.groups()[0] != name: + raise RuntimeError( + f"Found residue {rematch.groups()[0]} while processing residue {name}" + ) + for res in container: + for atom in res: + vx, vy, vz = (float(x) for x in fileobj.readline().split()) + atom.vx, atom.vy, atom.vz = vx, vy, vz + + if nres > 1: + return container + return templ + + #=================================================== + + @classmethod + def write(cls, lib, dest): + """ Writes a dictionary of ResidueTemplate units to a file in OFF format + + Parameters + ---------- + lib : dict {str : :class:`ResidueTemplate`} + Items can be either :class:`ResidueTemplate` or + :class:`ResidueTemplateContainer` instances + dest : str or file-like + Either a file name or a file-like object to write the file to + """ + own_handle = False + if not hasattr(dest, 'write'): + dest = genopen(dest, 'w') + own_handle = True + # Write the residues in alphabetical order + names = sorted(lib.keys()) + dest.write('!!index array str\n') + for name in names: + dest.write(' "%s"\n' % name) + for name in names: + cls._write_residue(dest, lib[name]) + + if own_handle: dest.close() + + #=================================================== + + @staticmethod + def _write_residue(dest, res): + """ Writes a residue to an open file handle + + Parameters + ---------- + dest : file-like + File object to write the residue information to + res : :class:`ResidueTemplate` or :class:`ResidueTemplateContainer` + The residue template (or template container) to write to the file + """ + if isinstance(res, ResidueTemplate): + # Put it into a template container with the same name + tmp = ResidueTemplateContainer(res.name) + tmp.append(res) + res = tmp + dest.write('!entry.%s.unit.atoms table str name str type int typex ' + 'int resx int flags int seq int elmnt dbl chg\n' % + res.name) + for i, r in enumerate(res): + for atom in r: + dest.write(' "%s" "%s" 0 %d 131072 %d %d %.6f\n' % (atom.name, + atom.type, i+1, atom.idx+1, atom.atomic_number, + atom.charge)) + dest.write('!entry.%s.unit.atomspertinfo table str pname str ptype ' + 'int ptypex int pelmnt dbl pchg\n' % res.name) + for r in res: + for atom in r: + dest.write(' "%s" "%s" 0 -1 0.0\n' % (atom.name, atom.type)) + dest.write('!entry.%s.unit.boundbox array dbl\n' % res.name) + if res.box is None: + dest.write((' -1.000000\n' + ' 0.0\n' * 4)) + else: + dest.write(' 1.000000\n') + if res.box[3] == res.box[4] == res.box[5]: + dest.write(' %f\n' % res.box[3]) + else: + raise RuntimeError('Cannot write boxes with different angles') + dest.write(' %f\n' % res.box[0]) + dest.write(' %f\n' % res.box[1]) + dest.write(' %f\n' % res.box[2]) + dest.write('!entry.%s.unit.childsequence single int\n %d\n' % + (res.name, len(res)+1)) + dest.write('!entry.%s.unit.connect array int\n' % res.name) + if len(res) > 1: + dest.write(' 0\n 0\n') + else: + if res[0].head is not None: + dest.write(' %d\n' % (res[0].head.idx + 1)) + else: + dest.write(' 0\n') + if res[0].tail is not None: + dest.write(' %d\n' % (res[0].tail.idx + 1)) + else: + dest.write(' 0\n') + if any(len(r) > 1 for r in res): + dest.write('!entry.%s.unit.connectivity table int atom1x ' + 'int atom2x int flags\n' % res.name) + base = 1 + for r in res: + for bond in r.bonds: + dest.write(' %d %d 1\n' % (bond.atom1.idx+base, + bond.atom2.idx+base)) + base += len(r) + dest.write('!entry.%s.unit.hierarchy table str abovetype int ' + 'abovex str belowtype int belowx\n' % res.name) + c = 1 + for i, r in enumerate(res): + dest.write(' "U" 0 "R" %d\n' % (i+1)) + for atom in r: + dest.write(' "R" %d "A" %d\n' % (i+1, c)) + c += 1 + dest.write('!entry.%s.unit.name single str\n' % res.name) + dest.write(' "%s"\n' % res.name) + dest.write('!entry.%s.unit.positions table dbl x dbl y dbl z\n' % + res.name) + for r in res: + for atom in r: + dest.write(' %.6g %.6g %.6g\n' % (atom.xx, atom.xy, atom.xz)) + dest.write('!entry.%s.unit.residueconnect table int c1x int c2x ' + 'int c3x int c4x int c5x int c6x\n' % res.name) + c = 1 + for r in res: + # Make the CONECT1 and 0 default to first and last atom so that the + # TREE gets set correctly by tleap. Not used for anything else... + conn = [c, c+len(r)-1, 0, 0, 0, 0] + if r.head is not None: conn[0] = r.head.idx + 1 + if r.tail is not None: conn[1] = r.tail.idx + 1 + for i, at in enumerate(r.connections[:4]): + conn[i+2] = at.idx + 1 + dest.write(' %d %d %d %d %d %d\n' % tuple(conn)) + c += len(r) + dest.write('!entry.%s.unit.residues table str name int seq int ' + 'childseq int startatomx str restype int imagingx\n' % + res.name) + c = 1 + for i, r in enumerate(res): + if r.type is PROTEIN: + typ = 'p' + elif r.type is NUCLEIC: + typ = 'n' + elif r.type is SOLVENT: + typ='w' + elif r.type is UNKNOWN: + typ='?' + else: + warnings.warn('Unrecognized residue type %r' % r.type, + AmberWarning) + typ = '?' + dest.write(' "%s" %d %d %d "%s" %d\n' % (r.name, i+1, 1+len(r), c, + typ, _imaging_atom(r)+c)) + c += len(r) + dest.write('!entry.%s.unit.residuesPdbSequenceNumber array int\n' % + res.name) + for i, r in enumerate(res): + if len(res) == 1: + dest.write(' 0\n') + else: + dest.write(' %d\n' % (i+1)) + dest.write('!entry.%s.unit.solventcap array dbl\n' % res.name) + dest.write(' -1.000000\n' + ' 0.0\n' * 4) + dest.write('!entry.%s.unit.velocities table dbl x dbl y dbl z\n' % + res.name) + for r in res: + for atom in r: + try: + s = ' %g %g %g\n' % (atom.vx, atom.vy, atom.vz) + except AttributeError: + dest.write(' 0.0 0.0 0.0\n') + else: + dest.write(s) + +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +# Helper routines +def _strip_enveloping_quotes(inp): + """ Strips the quotation marks enveloping a string """ + if inp[0] == inp[-1] == '"' or inp[0] == inp[-1] == "'": + return inp[1:-1] + return inp + +def _imaging_atom(res): + """ + Determines the imaging atom for the residue. If all atoms are hydrogen + except 1, it is the heavy atom. Otherwise, it is the atom *closest* to the + COM of the residue + """ + from ..geometry import center_of_mass + #TODO implement the docstring + found_heavy = False + heavy_idx = -1 + for i, atom in enumerate(res): + if atom.atomic_number > 1: + heavy_idx = i + if found_heavy: break + found_heavy = True + else: + if heavy_idx != -1: + return heavy_idx + return 0 # No heavy atoms?? No imaging atom, then. + coords = res.coordinates.reshape((len(res), 3)) + masses = np.zeros(len(res)) + for i, atom in enumerate(res): + if atom.mass == 0: + masses[i] = pt.Mass[pt.Element[atom.atomic_number]] + else: + masses[i] = atom.mass + com = center_of_mass(coords, masses) + diff = coords - com + return np.argmin((diff * diff).sum(axis=1)) diff --git a/pmx/workflow/parmed/amber/parameters.py b/pmx/workflow/parmed/amber/parameters.py new file mode 100644 index 00000000..bb8c12c0 --- /dev/null +++ b/pmx/workflow/parmed/amber/parameters.py @@ -0,0 +1,813 @@ +""" +This module contains classes for parsing and processing Amber parameter files. +""" +from collections import defaultdict +from contextlib import closing +import math +import os +import re +import warnings +from .offlib import AmberOFFLibrary +from ..constants import TINY +from ..exceptions import ParameterError, AmberWarning, ParameterWarning +from ..modeller.residue import ResidueTemplateContainer +from ..formats.mol2 import Mol2File +from ..formats.registry import FileFormatType +from ..parameters import ParameterSet +from ..periodic_table import Mass, element_by_mass, AtomicNum +from ..topologyobjects import AtomType, BondType, AngleType, DihedralType, DihedralTypeList +from ..utils.io import genopen +from collections.abc import Sequence + +# parameter file regexes +subs = dict(FLOATRE=r'([+-]?(?:\d+(?:\.\d*)?|\.\d+))', + FILENAMERE=r'''(".+?"|'.+?'|[\S]*)''') +_bondre = re.compile(r'(..?)-(..?)\s+%(FLOATRE)s\s+%(FLOATRE)s' % subs) +_anglere = re.compile(r'(..?)-(..?)-(..?)\s+%(FLOATRE)s\s+%(FLOATRE)s' % subs) +_dihedre = re.compile(r'(..?)-(..?)-(..?)-(..?)\s+%(FLOATRE)s\s+' + '%(FLOATRE)s\s+%(FLOATRE)s\s+%(FLOATRE)s' % subs) +_dihed2re = re.compile(r'\s*%(FLOATRE)s\s+%(FLOATRE)s\s+%(FLOATRE)s\s+' + '%(FLOATRE)s' % subs) +_sceere = re.compile(r'SCEE=\s*%(FLOATRE)s' % subs) +_scnbre = re.compile(r'SCNB=\s*%(FLOATRE)s' % subs) +_impropre = re.compile(r'(..?)-(..?)-(..?)-(..?)\s+' + '%(FLOATRE)s\s+%(FLOATRE)s\s+%(FLOATRE)s' % subs) +# Leaprc regexes +_atomtypere = re.compile(r"""({\s*["']([\w\+\-]+)["']\s*["'](\w+)["']\s*""" + r"""["'](\w+)["']\s*})""") +_loadparamsre = re.compile(r'loadamberparams\s+%(FILENAMERE)s' % subs, re.I) +_loadoffre = re.compile(r'loadoff\s+%(FILENAMERE)s' % subs, re.I) +_loadmol2re = re.compile(r'(\S+)\s*=\s*loadmol[23]\s*%(FILENAMERE)s' % subs, re.I) +del subs + +def _find_amber_file(fname, search_oldff): + """ + Finds an Amber file. Looks in the current directory, then the following + locations: + + - $AMBERHOME/dat/leap/lib + - $AMBERHOME/dat/leap/parm + """ + from . import AMBERHOME + if len(fname) > 1 and {fname[0], fname[-1]} in ({'"'}, {"'"}): + # Strip quotes + fname = fname[1:-1] + leapdir = os.path.join(AMBERHOME, 'dat', 'leap') + paths = [os.getcwd(), os.path.join(leapdir, 'lib'), os.path.join(leapdir, 'parm')] + if search_oldff: + paths.append(os.path.join(leapdir, 'lib', 'oldff')) + for path in paths: + if os.path.exists(os.path.join(path, fname)): + return os.path.join(path, fname) + raise FileNotFoundError('Cannot find Amber file [%s] in paths %s' % (fname, paths)) + +class AmberParameterSet(ParameterSet, metaclass=FileFormatType): + """ Class storing parameters from an Amber parameter set + + Parameters + ---------- + filenames : str, list of str, file-like, or list of file-like; optional + Either the name of a file or a list of filenames from which parameters + should be parsed. + + Notes + ----- + Order is important in the list of files provided. The parameters are loaded + in the order they are provided, and any parameters that are specified in + multiple places are overwritten (that is, the *last* occurrence is the + parameter type that is used) + + See Also + -------- + :class:`parmed.parameters.ParameterSet` + """ + + #=================================================== + + @staticmethod + def id_format(filename): + """ + Identifies the file type as either an Amber-style frcmod or parm.dat + file. + + Parameters + ---------- + filename : str + Name of the file to check format for + + Returns + ------- + is_fmt : bool + True if it is an Amber-style parameter file. False otherwise. + """ + with closing(genopen(filename, 'r')) as f: + f.readline() + line = f.readline() + if not line.strip(): # Must be an frcmod file + while line and not line.strip(): + line = f.readline() + if not line: + return False + if line.rstrip() not in ('MASS', 'BOND', 'ANGLE', 'ANGL', 'DIHE', 'DIHED', + 'DIHEDRAL', 'IMPR', 'IMPROP', 'IMPROPER', 'NONB', 'NONBON', + 'NONBOND', 'NONBONDED'): + return False + if line.rstrip() in ('MASS', 'BOND', 'ANGLE', 'ANGL', 'DIHE', 'DIHED', 'DIHEDRAL', + 'IMPR', 'IMPROP', 'IMPROPER', 'NONB', 'NONBON', 'NONBOND', + 'NONBONDED'): + return True # frcmod file + # This must be an atom definition in the parm.dat file + words = line.split() + if len(words) < 2: + return False + # The first word is the atom type (must be <= 2 characters, and not + # an integer) + if len(words[0]) > 2: return False + try: + float(words[0]) + return False + except ValueError: + pass + try: + float(words[1]) + except ValueError: + return False + # Polarizability might not be present... + try: + float(words[2]) + except (IndexError, ValueError): + # UGLY: Check the mass and make sure it matches the element's + # mass to within 1 amu. Do our best to guess the element. If + # it's a two-letter element, the element might be a 2-letter + # element (like Br), or it might be a 1-letter element specified + # by either the first or second letter. Check all possibilities. + # Special-case instances like CA, which are just as likely (more + # likely?) to be a carbon atom as it is to be a calcium (i.e., + # check for carbon atoms in anything that starts with C). If at + # any point, what *should* be the mass doesn't match the mass of + # the guessed element, tag this as *not* a parameter file + if len(words[0]) == 2: + if words[0][0].isalpha(): + if words[0][1].isalpha(): + key = words[0][0].upper() + words[0][1].lower() + if key in Mass and abs(Mass[key] - float(words[1])) > 1: + if key[0] == 'C' and abs(Mass['C'] - float(words[1])) > 1: + return False + elif key[0] != 'C': + return False + elif key not in Mass: + if key[0] in Mass and abs(Mass[key[0]] - float(words[1])) > 1: + return False + else: + return False + else: + key = words[0][0].upper() + if key in Mass and abs(Mass[key] - float(words[1])) > 1: + return False + elif key not in Mass: + return False + else: + key = words[0][1].upper() + if key in Mass: + if abs(Mass[key] - float(words[1])) > 1: + return False + else: + return False + else: + key = words[0][0].upper() + if key in Mass: + if abs(Mass[key] - float(words[1])) > 1: + return False + else: + return False + if len(words) > 3: + # Heuristic, but anything that comes after the polarizability is + # a comment, and I have yet to see a leading comment that is a + # number + try: + float(words[3]) + return False + except ValueError: + return True + else: + return True + + #=================================================== + + def __init__(self, *filenames): + super(AmberParameterSet, self).__init__() + self.default_scee = 1.2 + self.default_scnb = 2.0 + self.titles = [] + for filename in filenames: + if isinstance(filename, str): + if AmberOFFLibrary.id_format(filename): + self.residues.update(AmberOFFLibrary.parse(filename)) + else: + self.load_parameters(filename) + elif isinstance(filename, Sequence): + for fname in filename: + if AmberOFFLibrary.id_format(fname): + self.residues.update(AmberOFFLibrary.parse(fname)) + else: + self.load_parameters(fname) + else: + # Assume open file object + self.load_parameters(filename) + + #=================================================== + + @classmethod + def from_leaprc(cls, fname, search_oldff=False): + """ Load a parameter set from a leaprc file + + Parameters + ---------- + fname : str or file-like + Name of the file or open file-object from which a leaprc-style file + will be read + + search_oldff : bool, optional, default=False + If True, search the oldff directories in the main Amber leap + folders. Default is False + + Notes + ----- + This does not read all parts of a leaprc file -- only those pertinent to + defining force field information. For instance, the following sections + and commands are processed: + + - addAtomTypes + - loadAmberParams + - loadOFF + - loadMol2 + - loadMol3 + """ + params = cls() + if isinstance(fname, str): + f = genopen(fname, 'r') + own_handle = True + else: + f = fname + own_handle = False + # To make parsing easier, and because leaprc files are usually quite + # short, I'll read the whole file into memory + def joinlines(lines): + newlines = [] + composite = [] + for line in lines: + if line.endswith('\\\n'): + composite.append(line[:-2]) + continue + else: + composite.append(line) + newlines.append(''.join(composite)) + composite = [] + if composite: + newlines.append(''.join(composite)) + return newlines + lines = joinlines(map(lambda line: line if '#' not in line else line[:line.index('#')], f)) + text = ''.join(lines) + if own_handle: f.close() + lowertext = text.lower() # commands are case-insensitive + # Now process the parameter files + def process_fname(fname): + if fname[0] in ('"', "'"): + fname = fname[1:-1] + fname = fname.replace('_BSTOKEN_', r'\ ').replace(r'\ ', ' ') + return fname + for line in lines: + line = line.replace(r'\ ', '_BSTOKEN_') + if _loadparamsre.findall(line): + fname = process_fname(_loadparamsre.findall(line)[0]) + params.load_parameters(_find_amber_file(fname, search_oldff)) + elif _loadoffre.findall(line): + fname = process_fname(_loadoffre.findall(line)[0]) + params.residues.update(AmberOFFLibrary.parse(_find_amber_file(fname, search_oldff))) + elif _loadmol2re.findall(line): + (resname, fname), = _loadmol2re.findall(line) + residue = Mol2File.parse(_find_amber_file(fname, search_oldff)) + if isinstance(residue, ResidueTemplateContainer): + warnings.warn('Multi-residue mol2 files not supported by tleap. Loading anyway ' + 'using names in mol2', AmberWarning) + for res in residue: + params.residues[res.name] = res + else: + params.residues[resname] = residue + # Now process the addAtomTypes + try: + idx = lowertext.index('addatomtypes') + except ValueError: + # Does not exist in this file + atom_types_str = '' + else: + i = idx + len('addatomtypes') + while i < len(text) and text[i] != '{': + if text[i] not in '\r\n\t ': + raise ParameterError('Unsupported addAtomTypes syntax in leaprc file') + i += 1 + if i == len(text): + raise ParameterError('Unsupported addAtomTypes syntax in leaprc file') + # We are at our first brace + chars = [] + nopen = 1 + i += 1 + while i < len(text): + char = text[i] + if char == '{': + nopen += 1 + elif char == '}': + nopen -= 1 + if nopen == 0: break + elif char == '\n': + char = ' ' + chars.append(char) + i += 1 + atom_types_str = ''.join(chars).strip() + for _, name, symb, hyb in _atomtypere.findall(atom_types_str): + if symb not in AtomicNum: + raise ParameterError('%s is not a recognized element' % symb) + if name in params.atom_types: + params.atom_types[name].atomic_number = AtomicNum[symb] + return params + + #=================================================== + + @classmethod + def from_structure(cls, struct): + """ Extracts known parameters from a Structure instance + + Parameters + ---------- + struct : :class:`parmed.structure.Structure` + The parametrized ``Structure`` instance from which to extract + parameters into a ParameterSet + + Returns + ------- + params : :class:`ParameterSet` + The parameter set with all parameters defined in the Structure + """ + return super(AmberParameterSet, cls).from_structure(struct, allow_unequal_duplicates=False) + + #=================================================== + + def load_parameters(self, fname): + """ Load a set of parameters from a single parameter file + + Parameters + ---------- + fname : str or file-like + Parameter file to parse + """ + if isinstance(fname, str): + f = genopen(fname, 'r') + own_handle = True + else: + f = fname + own_handle = False + self.titles.append(f.readline().strip()) + try: + for line in f: + if not line.strip(): + return self._parse_frcmod(f, line) + elif line.strip() in ('MASS', 'BOND', 'ANGLE', 'ANGL', 'DIHE', 'DIHED', 'DIHEDRAL', + 'IMPR', 'IMPROP', 'IMPROPER', 'NONB', 'NONBON', 'NONBOND', + 'NONBONDED'): + return self._parse_frcmod(f, line) + else: + return self._parse_parm_dat(f, line) + finally: + if own_handle: + f.close() + + #=================================================== + + def _parse_frcmod(self, f, line): + """ Parses an frcmod file from an open file object """ + def fiter(): + yield line + for l in f: yield l + section = None + finished_diheds = defaultdict(lambda: True) + key = None + for line in fiter(): + line = line.rstrip() + if not line: continue + if line.startswith('MASS'): + section = 'MASS' + continue + elif line.startswith('BOND'): + section = 'BOND' + continue + elif line.startswith('ANGL'): + section = 'ANGLE' + continue + elif line.startswith('DIHE'): + section = 'DIHEDRAL' + continue + elif line.startswith('IMPR'): + section = 'IMPROPER' + continue + elif line.startswith('NONB'): + section = 'NONBOND' + continue + elif line.startswith('LJEDIT'): + section = 'NBFIX' + continue + + if section == 'MASS': + self._process_mass_line(line) + elif section == 'BOND': + self._process_bond_line(line) + elif section == 'ANGLE': + self._process_angle_line(line) + elif section == 'DIHEDRAL': + key = self._process_dihedral_line(line, finished_diheds, key) + elif section == 'IMPROPER': + self._process_improper_line(line) + elif section == 'NONBOND': + self._process_nonbond_line(line) + elif section == 'NBFIX': + self._process_nbfix_line(line) + + #=================================================== + + def _parse_parm_dat(self, f, line): + """ Internal parser for parm.dat files from open file handle """ + def fiter(): + yield line + for l in f: yield l + # Keep yielding empty string after file has ended + yield '' + fiter = fiter() + rawline = next(fiter) + finished_diheds = defaultdict(lambda: True) + # Parse the masses + while rawline: + line = rawline.strip() + if not line: + break + self._process_mass_line(line) + rawline = next(fiter) + next(fiter) # Skip the list of hydrophobic atom types + # Process the bonds + rawline = next(fiter) + while rawline: + line = rawline.strip() + if not line: + break + self._process_bond_line(line) + rawline = next(fiter) + # Process the angles + rawline = next(fiter) + while rawline: + line = rawline.strip() + if not line: + break + self._process_angle_line(line) + rawline = next(fiter) + # Process the dihedrals + rawline = next(fiter) + key = None + while rawline: + line = rawline.strip() + if not line: + break + key = self._process_dihedral_line(line, finished_diheds, key) + rawline = next(fiter) + # Process the impropers + rawline = next(fiter) + while rawline: + line = rawline.strip() + if not line: + break + self._process_improper_line(line) + rawline = next(fiter) + # Process the 10-12 terms + rawline = next(fiter) + while rawline: + line = rawline.strip() + if not line: + break + try: + a1, a2, acoef, bcoef = line.split()[:4] + acoef = float(acoef) + bcoef = float(bcoef) + except ValueError as err: + raise ParameterError('Trouble parsing 10-12 terms') from err + if acoef != 0 or bcoef != 0: + raise ParameterError('10-12 potential not supported in AmberParameterSet currently') + rawline = next(fiter) + # Process 12-6 terms. Get Equivalencing first + rawline = next(fiter) + equivalent_ljtypes = dict() + equivalent_types = defaultdict(list) + while rawline: + line = rawline.strip() + if not line: + break + words = line.split() + for typ in words[1:]: + equivalent_ljtypes[typ] = words[0] + equivalent_types[words[0]].append(typ) + rawline = next(fiter) + words = next(fiter).split() + if len(words) < 2: + raise ParameterError( + 'Could not parse the kind of nonbonded parameters in Amber parameter file' + ) + if words[1].upper() != 'RE': + raise ParameterError('Only RE nonbonded parameters supported') + rawline = next(fiter) + while rawline: + line = rawline.strip() + if not line: + break + self._process_nonbond_line(line) + rawline = next(fiter) + # Now assign all of the equivalenced atoms + for atyp, otyp in equivalent_ljtypes.items(): + otyp = self.atom_types[otyp] + if atyp in self.atom_types: + if (self.atom_types[atyp].rmin is not None and + self.atom_types[atyp].epsilon is not None): + if (abs(otyp.epsilon-self.atom_types[atyp].epsilon) > TINY + or abs(otyp.rmin-self.atom_types[atyp].rmin) > TINY): + warnings.warn( + f'{otyp.name} and {atyp} expected to be equal but are not', AmberWarning + ) + # Remove from equivalent types + equivalent_types[otyp.name].remove(atyp) + continue + self.atom_types[atyp].set_lj_params(otyp.epsilon, otyp.rmin) + line = next(fiter).strip() + if line == 'LJEDIT': + rawline = next(fiter) + while rawline: + line = rawline.strip() + if not line: + break + self._process_nbfix_line(line, equivalent_types) + rawline = next(fiter) + + #=================================================== + + # Private methods for processing parts of the file + def _process_mass_line(self, line): + words = line.split() + try: + mass = float(words[1]) + except ValueError: + raise ParameterError(f'Could not convert mass to float [{words[1]}]') + except IndexError: + raise ParameterError('Error parsing MASS line. Not enough tokens') + if words[0] in self.atom_types: + self.atom_types[words[0]].mass = mass + elif words[0] in ('EP', 'LP'): + atype = AtomType(words[0], len(self.atom_types)+1, mass, 0) + self.atom_types[words[0]] = atype + else: + atype = AtomType(words[0], len(self.atom_types)+1, mass, AtomicNum[element_by_mass(mass)]) + self.atom_types[words[0]] = atype + + #=================================================== + + def _process_bond_line(self, line): + rematch = _bondre.match(line) + if not rematch: + raise ParameterError(f'Could not understand BOND line [{line}]') + a1, a2, k, eq = rematch.groups() + a1 = a1.strip(); a2 = a2.strip() + typ = BondType(float(k), float(eq)) + self.bond_types[(a1, a2)] = typ + self.bond_types[(a2, a1)] = typ + + def _process_angle_line(self, line): + rematch = _anglere.match(line) + if not rematch: + raise ParameterError(f'Could not understand ANGLE line [{line}]') + a1, a2, a3, k, eq = rematch.groups() + a1 = a1.strip(); a2 = a2.strip(); a3 = a3.strip() + typ = AngleType(float(k), float(eq)) + self.angle_types[(a1, a2, a3)] = typ + self.angle_types[(a3, a2, a1)] = typ + + def _process_dihedral_line(self, line, finished_diheds, last_key): + """ Processes a dihedral line, possibly part of a multi-term dihedral + + Parameters + ---------- + line : str + Line of the file that contains a dihedral term + finished_diheds : dict + Dictionary of dihedral parameters whose final term has been read in + already (which means additional terms will overwrite, not add) + last_key : str or None + If not None, this is the key for the last dihedral type that should + be implied if the atom types are missing. Atom types seem to only be + required for the first term in a multi-term torsion definition + + Returns + ------- + key or None + If a negative periodicity indicates another term is coming, the + current key is returned so it can be passed as key to the next + _process_dihedral_call + """ + rematch = _dihedre.match(line) + if not rematch and last_key is None: + raise ParameterError(f'Could not understand DIHEDRAL line [{line}]') + elif not rematch: + rematch = _dihed2re.match(line) + if not rematch: + raise ParameterError(f'Could not understand DIHEDRAL line [{line}]') + div, k, phi, per = rematch.groups() + key = last_key + rkey = tuple(reversed(key)) + assert key in finished_diheds + if finished_diheds[key]: + raise AssertionError('Cannot have an implied torsion that has already finished!') + else: + a1, a2, a3, a4, div, k, phi, per = rematch.groups() + a1, a2, a3, a4 = a1.strip(), a2.strip(), a3.strip(), a4.strip() + key = (a1, a2, a3, a4) + rkey = (a4, a3, a2, a1) + if last_key is not None and (last_key != key and last_key != rkey): + warnings.warn( + f'Expecting next term in dihedral {last_key}, got dihedral {key}', + ParameterWarning, + ) + scee = [float(x) for x in _sceere.findall(line)] or [1.2] + scnb = [float(x) for x in _scnbre.findall(line)] or [2.0] + per = float(per) + typ = DihedralType(float(k)/float(div), abs(per), float(phi), scee[0], scnb[0]) + if finished_diheds[key]: + # This dihedral is already finished its definition, which means + # we go ahead and add a new one to override it + typs = DihedralTypeList() + typs.append(typ) + self.dihedral_types[key] = self.dihedral_types[rkey] = typs + else: + self.dihedral_types[key].append(typ) + finished_diheds[key] = finished_diheds[rkey] = per >= 0 + if per < 0: + return key + + def _process_improper_line(self, line): + rematch = _impropre.match(line) + if not rematch: + raise ParameterError(f'Could not understand IMPROPER line [{line}]') + a1, a2, a3, a4, k, phi, per = rematch.groups() + a1 = a1.strip(); a2 = a2.strip(); + a3 = a3.strip(); a4 = a4.strip() + # Pre-sort the improper types, assuming atom3 is the central atom (which + # it must be in Amber parameter files!!!!) + a1, a2, a4 = sorted([a1, a2, a4]) + key = (a1, a2, a3, a4) + self.improper_periodic_types[key] = DihedralType(float(k), float(per), float(phi)) + + def _process_nonbond_line(self, line): + try: + atyp, rmin, eps = line.split()[:3] + except ValueError as err: + raise ParameterError(f'Could not understand nonbond parameter line [{line}]') from err + try: + self.atom_types[atyp].set_lj_params(float(eps), float(rmin)) + except KeyError as err: + raise ParameterError(f'Atom type {atyp} not present in the database.') from err + except ValueError as err: + raise ParameterError( + f'Could not convert nonbond parameters to floats [{rmin}, {eps}]' + ) from err + + def _process_nbfix_line(self, line, equivalents=None): + try: + a1, a2, rmin1, eps1, rmin2, eps2 = line.split()[:6] + except ValueError as err: + raise ParameterError(f'Could not understand LJEDIT line [{line}]') from err + try: + rmin1 = float(rmin1) + eps1 = float(eps1) + rmin2 = float(rmin2) + eps2 = float(eps2) + except ValueError as err: + raise ParameterError('Could not convert LJEDIT parameters to floats.') from err + self.nbfix_types[(min(a1, a2), max(a1, a2))] = (math.sqrt(eps1*eps2), rmin1+rmin2) + if equivalents is not None: + # We need to add the same nbfixes to all atom types that are + # equivalent to the atom types defined in the LJEDIT line + for oa1 in equivalents[a1]: + self.nbfix_types[(min(oa1, a2), max(oa1, a2))] = (math.sqrt(eps1*eps2), rmin1+rmin2) + for oa2 in equivalents[a2]: + self.nbfix_types[(min(a1, oa2), max(a1, oa2))] = (math.sqrt(eps1*eps2), rmin1+rmin2) + # Now do the equivalenced of atom 1 with the equivalenced of atom 2 + for oa1 in equivalents[a1]: + for oa2 in equivalents[a2]: + self.nbfix_types[(min(oa1, oa2), max(oa1, oa2))] = ( + math.sqrt(eps1*eps2), rmin1+rmin2 + ) + + #=================================================== + + def write(self, dest, title='Created by ParmEd', style='frcmod'): + """ Writes a parm.dat file with the current parameters + + Parameters + ---------- + dest : str or file-like + The file name or file-like object to write the parameters to + title : str, optional + The title of the frcmod to write. Default is 'Created by ParmEd' + style : str, optional + If 'frcmod', the parameters are written in frcmod-format. If 'parm', + the parameters are written in parm.dat-format. Default is 'frcmod' + """ + if isinstance(dest, str): + outfile = genopen(dest, 'w') + own_handle = True + else: + outfile = dest + own_handle = False + + if style not in ('frcmod', 'parm'): + raise ValueError(f'style must be either frcmod or parm, not {style}') + + outfile.write(title.rstrip('\r\n')) + outfile.write('\n') + # Write the atom mass + outfile.write('MASS\n') + for atom, typ in self.atom_types.items(): + outfile.write(f'{atom:<6}{typ.mass:6.3f}\n') + outfile.write('\n') + # Write the bonds + outfile.write('BOND\n') + done = set() + for (a1, a2), typ in self.bond_types.items(): + if id(typ) in done: continue + done.add(id(typ)) + outfile.write(f'{a1:<2}-{a2:<2} {typ.k:8.3f} {typ.req:6.3f}\n') + outfile.write('\n') + # Write the angles + outfile.write('ANGLE\n') + done = set() + for (a1, a2, a3), typ in self.angle_types.items(): + if id(typ) in done: continue + done.add(id(typ)) + outfile.write(f"{a1:<2}-{a2:<2}-{a3:<2} {typ.k:8.3f} {typ.theteq:6.3f}\n") + outfile.write('\n') + # Write the dihedrals + outfile.write('DIHE\n') + done = set() + def write_dtype(outfile, a1, a2, a3, a4, cont, typ): + outfile.write( + f'{a1:<2}-{a2:<2}-{a3:<2}-{a4:<2} {1:4d} {typ.phi_k:14.8f} {typ.phase:8.3f} ' + f'{cont * typ.per:5.1f} SCEE={typ.scee} SCNB={typ.scnb}\n' + ) + for (a1, a2, a3, a4), typ in self.dihedral_types.items(): + if id(typ) in done: continue + done.add(id(typ)) + if isinstance(typ, DihedralType) or len(typ) == 1: + if not isinstance(typ, DihedralType): + typ = typ[0] + write_dtype(outfile, a1, a2, a3, a4, 1, typ) + else: + for dtyp in typ[:-1]: + write_dtype(outfile, a1, a2, a3, a4, -1, dtyp) + write_dtype(outfile, a1, a2, a3, a4, 1, typ[-1]) + outfile.write('\n') + # Write the impropers + outfile.write('IMPROPER\n') + written_impropers = dict() + for (a1, a2, a3, a4), typ in self.improper_periodic_types.items(): + # Make sure wild-cards come at the beginning + if a2 == 'X': + assert a4 == 'X', 'Malformed generic improper!' + a1, a2, a3, a4 = a2, a4, a3, a1 + elif a4 == 'X': + a1, a2, a3, a4 = a4, a1, a3, a2 + a1, a2, a4 = sorted([a1, a2, a4]) + if (a1, a2, a3, a4) in written_impropers: + if written_impropers[(a1, a2, a3, a4)] != typ: + raise ValueError('Multiple impropers with the same atom set not allowed') + continue + outfile.write( + f'{a1:<2}-{a2:<2}-{a3:<2}-{a4:<2} {typ.phi_k:14.8f} {typ.phase:8.3f} {typ.per:5.1f}\n' + ) + written_impropers[(a1, a2, a3, a4)] = typ + outfile.write('\n') + # Write the LJ terms + outfile.write('NONB\n') + for atom, typ in self.atom_types.items(): + outfile.write(f'{atom:<2} {typ.rmin:12.8f} {typ.epsilon:12.8f}\n') + outfile.write('\n') + # Write the NBFIX terms + if self.nbfix_types: + outfile.write('LJEDIT\n') + for (a1, a2), (eps, rmin) in self.nbfix_types.items(): + outfile.write( + f'{a1:<2} {a2:<2} {eps:12.8f} {rmin / 2:12.8f} {eps:12.8f} {rmin / 2:12.8f}\n' + ) + + if own_handle: + outfile.close() diff --git a/pmx/workflow/parmed/amber/readparm.py b/pmx/workflow/parmed/amber/readparm.py new file mode 100644 index 00000000..f1fea199 --- /dev/null +++ b/pmx/workflow/parmed/amber/readparm.py @@ -0,0 +1,72 @@ +""" +This module is simply a namespace for all Amber topology-like classes. There are +different variants; for instance the standard Amber topology, the chamber-made +topology, and the tinker_to_amber-made topology. These classes are all defined +in their own private modules, but imported here to simplify the API. +""" + +from ..constants import PrmtopPointers +from .amberformat import AmberFormat +from ._amberparm import AmberParm, Rst7 +from ._chamberparm import ChamberParm, ConvertFromPSF +from ._tinkerparm import AmoebaParm, BeemanRestart + +__all__ = ['AmberFormat', 'AmberParm', 'ChamberParm', 'LoadParm', 'Rst7'] + +# Supply a function to load a topology file in the 'correct' format +def LoadParm(parmname, xyz=None, box=None): + """ + Loads a topology file using the correct class. + + Parameters + ---------- + parmname : ``str`` + The name of the topology file to load + xyz : str or array, optional + If provided, the coordinates, velocities and unit cell dimensions from the provided + Amber inpcrd/restart file will be loaded into the molecule, or the + coordinates will be loaded from the coordinate array + box : array, optional + If provided, the unit cell information will be set from the provided + unit cell dimensions (a, b, c, alpha, beta, and gamma, respectively) + + Returns + ------- + parm : :class:`AmberParm` (or subclass) + This function parses the topology file, determines if it is an + Amber-style (i.e., *traditional* Amber force field), Chamber-style + (i.e., CHARMM force field), or Amoeba-style (i.e., Amoeba force field), + and then returns an instance of the appropriate type. + """ + from .. import load_file + parm = AmberFormat(parmname) + if 'CTITLE' in parm.flag_list: + parm = parm.view_as(ChamberParm) + elif 'AMOEBA_FORCEFIELD' in parm.flag_list: + parm = parm.view_as(AmoebaParm) + else: + parm = parm.view_as(AmberParm) + + if isinstance(xyz, str): + f = load_file(xyz) + if not hasattr(f, 'coordinates') or f.coordinates is None: + raise TypeError(f'{xyz} does not have coordinates') + parm.coordinates = f.coordinates + if hasattr(f, 'velocities') and f.velocities is not None: + parm.velocities = f.velocities + if hasattr(f, 'box') and f.box is not None and box is None: + parm.box = f.box + + else: + parm.coordinates = xyz + if box is not None: + parm.box = box + + # If all else fails, set the box from the prmtop file + if parm.parm_data['POINTERS'][PrmtopPointers.IFBOX] > 0 and parm.box is None: + box = parm.parm_data['BOX_DIMENSIONS'] + parm.box = list(box[1:]) + [box[0], box[0], box[0]] + + parm.hasbox = parm.box is not None + + return parm diff --git a/pmx/workflow/parmed/amber/titratable_residues.py b/pmx/workflow/parmed/amber/titratable_residues.py new file mode 100644 index 00000000..38ccea4d --- /dev/null +++ b/pmx/workflow/parmed/amber/titratable_residues.py @@ -0,0 +1,1117 @@ +""" +This module contains all of the information for the titratable residues, +including reference energies, model compound pKas, and charge vectors for every +titratable residue treated. +""" +from math import log +from io import StringIO +from ..exceptions import AmberWarning, AmberError +import warnings + +titratable_residues = ['AS4', 'GL4', 'CYS', 'TYR', 'HIP', 'LYS', 'DAP', 'DCP', + 'DG', 'DT', 'AP', 'CP', 'G', 'U', 'HEH', 'PRN', 'TYX'] + +class _State(object): + """ A protonation state """ + sort_by_resnum = True + + def __init__(self, charges, refene, refene_old=None, protcnt=None, pka_corr=None, eleccnt=None, eo_corr=None): + self.charges = charges + self.refene = refene + self.refene_old = refene_old + self.protcnt = protcnt + self.pka_corr = pka_corr + self.eleccnt = eleccnt + self.eo_corr = eo_corr + +class _ReferenceEnergy(object): + """ Reference energies for various solvent models """ + LN_TO_LOG = log(10.0) + KB = 0.00199 + TEMP = 300.0 + def __init__(self, igb1=None, igb2=None, igb5=None, igb7=None, igb8=None): + self.pKa_is_set = False + self.igb1 = igb1 + self.igb2 = igb2 + self.igb5 = igb5 + self.igb7 = igb7 + self.igb8 = igb8 + + def solvent_energies(self, igb1=None, igb2=None, igb5=None, igb7=None, igb8=None): + """ + Add solvent reference energies, copying the GB reference energies if + none are explicitly given + """ + self.solvent = _ReferenceEnergy(igb1, igb2, igb5, igb7, igb8) + + def dielc2_energies(self, igb1=None, igb2=None, igb5=None, igb7=None, igb8=None): + """ + Add reference energies for a dielectric constant of 2.0. Since this has + no reason to be near the original reference energies, do not use those + in place of energies that aren't provided + """ + self.dielc2 = _ReferenceEnergy(igb1, igb2, igb5, igb7, igb8) + + def set_pKa(self, pKa, deprotonated=False): + """ + Adjusts the reference energies based on the pKa. If the reference energy + is given for a deprotonated state, the pKa adjustment is subtracted from + the reference energy. Otherwise, this state is a 'protonated' state, so + the pKa adjustment is added to the reference energy + """ + if self.pKa_is_set: return + self.pKa_is_set = True + factor = self.KB * self.LN_TO_LOG * self.TEMP * pKa + + if deprotonated: + if self.igb1 is not None: self.igb1 -= factor + if self.igb2 is not None: self.igb2 -= factor + if self.igb5 is not None: self.igb5 -= factor + if self.igb7 is not None: self.igb7 -= factor + if self.igb8 is not None: self.igb8 -= factor + else: + if self.igb1 is not None: self.igb1 += factor + if self.igb2 is not None: self.igb2 += factor + if self.igb5 is not None: self.igb5 += factor + if self.igb7 is not None: self.igb7 += factor + if self.igb8 is not None: self.igb8 += factor + # Now apply the correction to our solvent reference energies if we have + # them (and dielectric-2 energies, if we have them) + if hasattr(self, 'solvent'): + self.solvent.set_pKa(pKa, deprotonated) + if hasattr(self, 'dielc2'): + self.dielc2.set_pKa(pKa, deprotonated) + +class _LineBuffer(object): + """ Buffer to add lines to the cpin file """ + + CHARS_PER_LINE = 80 + + def __init__(self, file): + self.file = file + self.linebuffer = '' + + def add_word(self, word): + if len(self.linebuffer) + len(word) > self.CHARS_PER_LINE: + self.file.write(f"{self.linebuffer}\n") + self.linebuffer = f' {word}' + else: + self.linebuffer += word + + def add_words(self, words, space_delimited=False): + """ Adds multiple words """ + extra = '' + if space_delimited: + extra = ' ' + for word in words: + self.add_word(word + extra) + + def flush(self): + """ Flushes this buffer to the file """ + if len(self.linebuffer) == 0: + return + self.file.write(self.linebuffer + '\n') + self.linebuffer = '' + +class TitratableResidue(object): + """ + A residue with different protonation states defined for Amber for use in + the Constant pH MD method implemented in sander + """ + + def __init__(self, resname, atom_list, typ, pka=None, eo=None): + self.resname = resname + self.atom_list = list(atom_list) # list of atom names + self.states = [] + self.first_state = -1 + self.first_charge = -1 + self.typ = typ + self.pKa = pka + self.Eo = eo + + def _str_refenes(self, solvent=False, igb=2, dielc=1.0): + """ + Converts all reference energies into a formatted string with a message + saying if the energy is not set + """ + ret_str = '' + igb_str = f'igb{igb}' + for state in self.states: + if dielc == 2: + if solvent: + refene = getattr(state.refene.dielc2.solvent, igb_str) + else: + refene = getattr(state.refene.dielc2, igb_str) + else: + if solvent: + refene = getattr(state.refene.solvent, igb_str) + else: + refene = getattr(state.refene, igb_str) + + if refene is None: + ret_str += f"{'Not Set':>12s}" + else: + ret_str += f"{refene:12.5f}" + + return ret_str + + def __str__(self): + delineator = '-' * (8 + 12 * len(self.states)) + if self.typ == "ph": + ret_strs = [f"{self.resname:<4s}\tpKa = {self.pKa:5.1f}"] + elif self.typ == "redox": + ret_strs = [f"{self.resname:<4s}\tEo = {self.Eo:7.3f} V"] + else: + ret_strs = [f"{self.resname:<4s}"] + ret_strs.append( + f"{'ATOM':>8s}" + ''.join([f"STATE {i}".rjust(12) for i in range(len(self.states))]) + ) + for i, atom in enumerate(self.atom_list): + ret_strs.append(f"{atom:>8s}" + "".join([f"{state.charges[i]:12.4f}" for state in self.states])) + ret_strs.append(delineator) + if self.typ == "ph" or self.typ == "phredox": + ret_strs.append("Prot Cnt".rjust(8) + "".join([f"{state.protcnt:12d}" for state in self.states])) + ret_strs.append(delineator) + ret_strs.append("pKa Corr".rjust(8) + "".join([f"{state.pka_corr:12.4f}" for state in self.states])) + if (self.typ == "phredox"): + ret_strs.append(delineator) + if (self.typ == "redox" or self.typ == "phredox"): + ret_strs.append("Elec Cnt".rjust(8) + "".join([f"{state.eleccnt:12d}" for state in self.states])) + ret_strs.append(delineator) + ret_strs.append("Eo Corr".rjust(8) + "".join([f"{state.eo_corr:12.4f}" for state in self.states])) + ret_strs.append(delineator) + ret_strs.extend(["Reference Energies (ES = Explicit solvent, IS = Implicit solvent)", ""]) + ret_strs.append(f"{'igb=1 IS':8s}{self._str_refenes(False, 1)}") + ret_strs.append(f"{'igb=2 IS':8s}{self._str_refenes(False, 2)}") + ret_strs.append(f"{'igb=5 IS':8s}{self._str_refenes(False, 5)}") + ret_strs.append(f"{'igb=7 IS':8s}{self._str_refenes(False, 7)}") + ret_strs.append(f"{'igb=8 IS':8s}{self._str_refenes(False, 8)}") + ret_strs.append(f"{'igb=1 ES':8s}{self._str_refenes(True, 1)}") + ret_strs.append(f"{'igb=2 ES':8s}{self._str_refenes(True, 2)}") + ret_strs.append(f"{'igb=5 ES':8s}{self._str_refenes(True, 5)}") + ret_strs.append(f"{'igb=7 ES':8s}{self._str_refenes(True, 7)}") + ret_strs.append(f"{'igb=8 ES':8s}{self._str_refenes(True, 8)}") + ret_strs.append(delineator) + ret_strs.extend(["Reference Energies for Internal Dielectric of 2.0", ""]) + ret_strs.append(f"{'igb=1 IS':8s}{self._str_refenes(False, 1, 2)}") + ret_strs.append(f"{'igb=2 IS':8s}{self._str_refenes(False, 2, 2)}") + ret_strs.append(f"{'igb=5 IS':8s}{self._str_refenes(False, 5, 2)}") + ret_strs.append(f"{'igb=7 IS':8s}{self._str_refenes(False, 7, 2)}") + ret_strs.append(f"{'igb=8 IS':8s}{self._str_refenes(False, 8, 2)}") + ret_strs.append(f"{'igb=1 ES':8s}{self._str_refenes(True, 1, 2)}") + ret_strs.append(f"{'igb=2 ES':8s}{self._str_refenes(True, 2, 2)}") + ret_strs.append(f"{'igb=5 ES':8s}{self._str_refenes(True, 5, 2)}") + ret_strs.append(f"{'igb=7 ES':8s}{self._str_refenes(True, 7, 2)}") + ret_strs.append(f"{'igb=8 ES':8s}{self._str_refenes(True, 8, 2)}") + return "\n".join(ret_strs) + "\n" + + def add_state(self, charges, refene, refene_old=None, protcnt=None, pka_corr=None, eleccnt=None, eo_corr=None): + """ Add a single titratable state for this titratable residue """ + new_state = _State(charges, refene, refene_old=refene_old, protcnt=protcnt, pka_corr=pka_corr, eleccnt=eleccnt, eo_corr=eo_corr) + if len(new_state.charges) != len(self.atom_list): + raise AmberError('Wrong number of charges for new state') + self.states.append(new_state) + + def add_states(self, charges, refenes, refenes_old=None, protcnts=None, pka_corrs=None, eleccnts=None, eo_corrs=None): + """ Add multiple titratable states for this titratable residue """ + if len(charges) != len(refenes) or (len(charges) != len(refenes_old) and refenes_old) or (len(charges) != len(protcnts) and protcnts) \ + or (len(charges) != len(pka_corrs) and pka_corrs) or (len(charges) != len(eleccnts) and eleccnts) \ + or (len(charges) != len(eo_corrs) and eo_corrs): + raise AmberError('Inconsistent list of parameters for TitratableResidue.add_states') + for i in range(len(charges)): + self.add_state(charges[i], refenes[i], refenes_old[i], protcnts[i], pka_corrs[i], eleccnts[i], eo_corrs[i]) + + def cpin_pointers(self, first_atom): + """ Sets and returns the cpin info """ + if self.first_state == -1 or self.first_charge == -1: + raise AmberError('Must set residue pointers before writing cpin info!') + return {'FIRST_ATOM': first_atom, + 'FIRST_CHARGE': self.first_charge, + 'FIRST_STATE': self.first_state, + 'NUM_ATOMS': len(self.atom_list), + 'NUM_STATES': len(self.states)} + + def set_first_state(self, index): + """ Sets the first state index """ + # Has the first state already been set? + if self.first_state != -1: + if index != self.first_state: + raise AmberError('First state already set differently') + self.first_state = index + + def set_first_charge(self, index): + """ Sets the first charge index """ + # Has it already been set? + if self.first_charge != -1: + if index != self.first_charge: + raise AmberError('First charge already set differently') + self.first_charge = index + + def reset(self): + """ Resets the pointers """ + self.first_state = -1 + self.first_charge = -1 + + def check(self): + """ Checks that the charges are consistent w/ the protonation states """ + sum_charges = [sum(state.charges) for state in self.states] + if (self.typ == "ph" or self.typ == "phredox"): + protcnts = [state.protcnt for state in self.states] + if (self.typ == "redox" or self.typ == "phredox"): + eleccnts = [state.eleccnt for state in self.states] + # All we have to do is make sure that the charges/proton counts are + # consistent between the first state and every other state + for i in range(1, len(sum_charges)): + charge_diff = sum_charges[i] - sum_charges[0] + if (self.typ == "ph"): + diff = protcnts[i] - protcnts[0] + elif (self.typ == "redox"): + diff = eleccnts[0] - eleccnts[i] + elif (self.typ == "phredox"): + diff = protcnts[i] - protcnts[0] + eleccnts[0] - eleccnts[i] + if abs(charge_diff - diff) >= 0.0001: + warnings.warn(f'Charge definitions inconsistent in {self.resname}', AmberWarning) + # Check all of the reference energies to make sure that the pKa was set + # for all but one of them + notset = 0 + valid = True + for state in self.states: + if not state.refene_old: + valid = False + break + notset += int(state.refene_old.pKa_is_set) + if (notset != len(self.states) - 1 and valid): + warnings.warn(f"Not enough states are pKa-adjusted in {self.resname}") + +class TitratableResidueList(list): + """ List of all titratable residues """ + def __init__(self, system_name='Unknown', solvated=False, first_solvent=0): + list.__init__(self) + self.first_atoms = [] + self.residue_nums = [] + self.resstates = [] + self.system_name = system_name + self.solvated = solvated + self.first_sol = first_solvent + + def add_residue(self, residue, resnum, first_atom, state=0): + """ Adds a residue to the list """ + list.append(self, residue) + self.first_atoms.append(first_atom) + self.residue_nums.append(resnum) + if state < 0 or state >= len(residue.states): + raise AmberError( + f"Residue {residue.resname} only has states 0-{len(residue.states)} ({state} chosen)" + ) + self.resstates.append(state) + + def set_states(self, statelist): + """ + Sets the initial protonation states from a list -- make sure there are + enough states in the list to set every residue, or emit a warning + """ + if len(statelist) != len(self): + warnings.warn( + f"Number of states ({len(statelist)}) does not equal number of " + f"residues ({len(self)}). Using default initial states.", AmberWarning + ) + return + # Check that all states are allowable + for i, state in enumerate(statelist): + if state < 0 or state >= len(self[i].states): + raise AmberError(f"Bad state choice ({state}). Minimum is 0, maximum is {len(self[i].states)}") + # If we got here, then we are OK + self.resstates = statelist + + def sort(self): + """ Sorts by residue number """ + atoms_residues = sorted(list(zip(self.first_atoms, self.residue_nums))) + self.first_atoms = [atom_residue[0] for atom_residue in atoms_residues] + self.residue_nums = [atom_residue[1] for atom_residue in atoms_residues] + + def write_cpin(self, output, igb=2, intdiel=1.0, oldfmt=False, typ="ph", coions=False): + """ Writes the CPIN file based on the titrated residues """ + end = StringIO() + # Reset all residues + for res in self: res.reset() + # Sort our residue list + self.sort() + limit_buf = _LineBuffer(output) + buf = _LineBuffer(end) + limit_buf.add_word('&CNSTPHE_LIMITS') + limit_buf.flush() + limit_buf.add_word(f' ntres={len(self)},') + limit_buf.add_word(f' maxh={max(len(r.states) for r in self)},') + if (typ == "ph"): + buf.add_word('&CNSTPH') + elif (typ == "redox"): + buf.add_word('&CNSTE') + elif (typ == "phredox"): + buf.add_word('&CNSTPHE') + buf.flush() + buf.add_word(' CHRGDAT=') + charges, energies, protcnts, pka_corrs, eleccnts, eo_corrs, pointers = [], [], [], [], [], [], [] + first_charge = 0 + first_state = 0 + for i, res in enumerate(self): + if res.first_charge == -1: + res.set_first_charge(first_charge) + res.set_first_state(first_state) + + for state in res.states: + # See which dielectric reference energies we want + if intdiel == 2: + if (not oldfmt): + refene = state.refene.dielc2 + else: + refene = state.refene_old.dielc2 + else: + if (not oldfmt): + refene = state.refene + else: + refene = state.refene_old + # See if we want the explicit solvent refene or not + if self.solvated: + energies.append(getattr(refene.solvent, f"igb{igb}")) + else: + energies.append(getattr(refene, f"igb{igb}")) + if (typ == "ph" or typ == "phredox"): + # Add protonation count of this state + if (state.protcnt): + protcnts.append(state.protcnt) + else: + protcnts.append(0) + # Add pka reference of this state + if (state.pka_corr): + pka_corrs.append(state.pka_corr) + else: + pka_corrs.append(0.0) + if (typ == "redox" or typ == "phredox"): + # Add electron count of this state + if (state.eleccnt): + eleccnts.append(state.eleccnt) + else: + eleccnts.append(0) + # Add Eo reference of this state + if (state.eo_corr): + eo_corrs.append(state.eo_corr) + else: + eo_corrs.append(0.0) + + first_state += len(res.states) + new_charges = [] + for state in res.states: + new_charges.extend(state.charges) + charges.extend(new_charges) + first_charge += len(new_charges) + pointers.append(res.cpin_pointers(self.first_atoms[i])) + + limit_buf.add_word(f' natchrg={len(charges)},') + limit_buf.add_word(f' ntstates={max(len(protcnts), len(eleccnts))},') + limit_buf.flush() + limit_buf.add_word('/') + limit_buf.flush() + # Print the charges + for charge in charges: + buf.add_word(f'{charge},') + buf.flush() + # Print the protcnts + if (typ == "ph" or typ == "phredox"): + buf.add_word(' PROTCNT=') + for protcnt in protcnts: + buf.add_word(f'{protcnt:d},') + buf.flush() + if (typ == "redox" or typ == "phredox"): + buf.add_word(' ELECCNT=') + for eleccnt in eleccnts: + buf.add_word('%d,' % eleccnt) + buf.flush() + # Print the residue names + buf.add_word(f" RESNAME='System: {self.system_name}',") + for i, res in enumerate(self): + buf.add_word(f"'Residue: {res.resname} {self.residue_nums[i]:d}',") + buf.flush() + # Print the residue states + buf.add_word(" RESSTATE=") + for state in self.resstates: + buf.add_word(f'{state:d},') + buf.flush() + # Print the residue pointers + buf.add_word(' ') # get a leading space + for i, p in enumerate(pointers): + buf.add_word(f"STATEINF({i})%FIRST_ATOM={p['FIRST_ATOM']}, ") + buf.add_word(f"STATEINF({i})%FIRST_CHARGE={p['FIRST_CHARGE']}, ") + buf.add_word(f"STATEINF({i})%FIRST_STATE={p['FIRST_STATE']}, ") + buf.add_word(f"STATEINF({i})%NUM_ATOMS={p['NUM_ATOMS']}, ") + buf.add_word(f"STATEINF({i})%NUM_STATES={p['NUM_STATES']}, ") + buf.flush() + # Print the reference energies + buf.add_word(' STATENE=') + for i, energy in enumerate(energies): + if energy is None: + raise AmberError(f"{i}'th reference energy not known for igb = {igb}") + buf.add_word(f'{energy:.6f},') + buf.flush() + # Print the pKa or Eo reference + if not oldfmt: + if typ == "ph" or typ == "phredox": + buf.add_word(' PKA_CORR=') + for pka_corr in pka_corrs: + buf.add_word(f'{pka_corr:.4f},') + buf.flush() + if (typ == "redox" or typ == "phredox"): + buf.add_word(' EO_CORR=') + for eo_corr in eo_corrs: + buf.add_word(f'{eo_corr:.4f},') + buf.flush() + # Print the # of residues and explicit solvent info if required + buf.add_word(' TRESCNT=%d,' % len(self)) + if self.solvated: + if (typ == "ph"): + buf.add_word(f'CPHFIRST_SOL={self.first_sol}, CPH_IGB={igb}, CPH_INTDIEL={intdiel}, ') + elif (typ == "redox"): + buf.add_word(f'CEFIRST_SOL={self.first_sol}, CE_IGB={igb}, CE_INTDIEL={intdiel}, ') + elif (typ == "phredox"): + buf.add_word(f'CPHEFIRST_SOL={self.first_sol}, CPHE_IGB={igb}, CPHE_INTDIEL={intdiel}, ') + buf.flush() + # Now scan through all of the waters + buf.flush() + buf.add_word('/') + buf.flush() + end.seek(0) + output.write(end.read()) + +# Now define all of the titratable residues + +# Aspartate +refene1 = _ReferenceEnergy(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene1.solvent_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene1.dielc2_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene1.dielc2.solvent_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene2 = _ReferenceEnergy(igb1=21.4298008, igb2=26.8894581, igb5=26.5980488, + igb7=23.4181107, igb8=26.3448911) +refene2.solvent_energies(igb2=33.2613028, igb5=32.064349, igb7=28.262350, igb8=31.286037) +refene2.dielc2_energies(igb2=12.676908, igb5=13.084913) +refene2.dielc2.solvent_energies() +# Copying the reference energy to be printted on the old CPIN format +refene2_old = _ReferenceEnergy(igb1=21.4298008, igb2=26.8894581, igb5=26.5980488, + igb7=23.4181107, igb8=26.3448911) +refene2_old.solvent_energies(igb2=33.2613028, igb5=32.064349, igb7=28.262350, igb8=31.286037) +refene2_old.dielc2_energies(igb2=12.676908, igb5=13.084913) +refene2_old.dielc2.solvent_energies() +refene2_old.set_pKa(4.0, deprotonated=False) + +AS4 = TitratableResidue('AS4', ['N', 'H', 'CA', 'HA', 'CB', 'HB2', 'HB3', 'CG', + 'OD1', 'OD2', 'HD21', 'C', 'O', 'HD22', 'HD11', 'HD12'], + pka=4.0, typ="ph") +AS4.add_state(protcnt=0, refene=refene1, refene_old=refene1, pka_corr=0.0, # deprotonated + charges=[-0.4157, 0.2719, 0.0341, 0.0864, -0.1783, -0.0122, + -0.0122, 0.7994, -0.8014, -0.8014, 0.0, 0.5973, -0.5679, 0.0, 0.0, + 0.0]) +AS4.add_state(protcnt=1, refene=refene2, refene_old=refene2_old, pka_corr=4.0, # protonated syn-O2 + charges=[-0.4157, 0.2719, 0.0341, 0.0864, -0.0316, 0.0488, 0.0488, + 0.6462, -0.5554, -0.6376, 0.4747, 0.5973, -0.5679, 0.0, 0.0, 0.0]) +AS4.add_state(protcnt=1, refene=refene2, refene_old=refene2_old, pka_corr=4.0, # protonated anti-O2 + charges=[-0.4157, 0.2719, 0.0341, 0.0864, -0.0316, 0.0488, 0.0488, + 0.6462, -0.5554, -0.6376, 0.0, 0.5973, -0.5679, 0.4747, 0.0, 0.0]) +AS4.add_state(protcnt=1, refene=refene2, refene_old=refene2_old, pka_corr=4.0, # protonated syn-O1 + charges=[-0.4157, 0.2719, 0.0341, 0.0864, -0.0316, 0.0488, 0.0488, + 0.6462, -0.6376, -0.5554, 0.0, 0.5973, -0.5679, 0.0, 0.4747, 0.0]) +AS4.add_state(protcnt=1, refene=refene2, refene_old=refene2_old, pka_corr=4.0, # protonated anti-O1 + charges=[-0.4157, 0.2719, 0.0341, 0.0864, -0.0316, 0.0488, 0.0488, + 0.6462, -0.6376, -0.5554, 0.0, 0.5973, -0.5679, 0.0, 0.0, 0.4747]) +AS4.check() # check that everything is consistent + +# Glutamate +refene1 = _ReferenceEnergy(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene1.solvent_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene1.dielc2_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene1.dielc2.solvent_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene2 = _ReferenceEnergy(igb1=3.89691326, igb2=8.4057785, igb5=8.0855764, + igb7=5.305949, igb8=8.3591335) +refene2.solvent_energies(igb2=15.20019319, igb5=13.533409, igb7=10.682953, igb8=13.242410) +refene2.dielc2_energies(igb2=3.455596, igb5=3.957270) +refene2.dielc2.solvent_energies() +# Copying the reference energy to be printted on the old CPIN format +refene2_old = _ReferenceEnergy(igb1=3.89691326, igb2=8.4057785, igb5=8.0855764, + igb7=5.305949, igb8=8.3591335) +refene2_old.solvent_energies(igb2=15.20019319, igb5=13.533409, igb7=10.682953, igb8=13.242410) +refene2_old.dielc2_energies(igb2=3.455596, igb5=3.957270) +refene2_old.dielc2.solvent_energies() +refene2_old.set_pKa(4.4, deprotonated=False) + +GL4 = TitratableResidue('GL4', ['N', 'H', 'CA', 'HA', 'CB', 'HB2', 'HB3', 'CG', + 'HG2', 'HG3', 'CD', 'OE1', 'OE2', 'HE21', 'C', 'O', + 'HE22', 'HE11', 'HE12'], pka=4.4, typ="ph") +GL4.add_state(protcnt=0, refene=refene1, refene_old=refene1, pka_corr=0.0, # deprotonated + charges=[-0.4157, 0.2719, 0.0145, 0.0779, -0.0398, -0.0173, + -0.0173, 0.0136, -0.0425, -0.0425, 0.8054, -0.8188, -0.8188, 0.0, + 0.5973, -0.5679, 0.0, 0.0, 0.0]) +GL4.add_state(protcnt=1, refene=refene2, refene_old=refene2_old, pka_corr=4.4, # protonated syn-O2 + charges=[-0.4157, 0.2719, 0.0145, 0.0779, -0.0071, 0.0256, 0.0256, + -0.0174, 0.0430, 0.0430, 0.6801, -0.5838, -0.6511, 0.4641, 0.5973, + -0.5679, 0.0, 0.0, 0.0]) +GL4.add_state(protcnt=1, refene=refene2, refene_old=refene2_old, pka_corr=4.4, # protonated anti-O2 + charges=[-0.4157, 0.2719, 0.0145, 0.0779, -0.0071, 0.0256, 0.0256, + -0.0174, 0.0430, 0.0430, 0.6801, -0.5838, -0.6511, 0.0, 0.5973, + -0.5679, 0.4641, 0.0, 0.0]) +GL4.add_state(protcnt=1, refene=refene2, refene_old=refene2_old, pka_corr=4.4, # protonated syn-O1 + charges=[-0.4157, 0.2719, 0.0145, 0.0779, -0.0071, 0.0256, 0.0256, + -0.0174, 0.0430, 0.0430, 0.6801, -0.6511, -0.5838, 0.0, 0.5973, + -0.5679, 0.0, 0.4641, 0.0]) +GL4.add_state(protcnt=1, refene=refene2, refene_old=refene2_old, pka_corr=4.4, # protonated syn-O2 + charges=[-0.4157, 0.2719, 0.0145, 0.0779, -0.0071, 0.0256, 0.0256, + -0.0174, 0.0430, 0.0430, 0.6801, -0.6511, -0.5838, 0.0, 0.5973, + -0.5679, 0.0, 0.0, 0.4641]) +GL4.check() + +# Tyrosine +refene1 = _ReferenceEnergy(igb2=0, igb5=0, igb8=0) +refene1.solvent_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene1.dielc2_energies(igb2=0, igb5=0, igb8=0) +refene1.dielc2.solvent_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene2 = _ReferenceEnergy(igb2=-65.113428, igb5=-64.166385, igb8=-61.3305355) +refene2.solvent_energies(igb2=-65.003415, igb5=-64.047229) +refene2.dielc2_energies(igb2=-32.167520, igb5=-31.751177) +refene2.dielc2.solvent_energies() +# Copying the reference energy to be printted on the old CPIN format +refene2_old = _ReferenceEnergy(igb2=-65.113428, igb5=-64.166385, igb8=-61.3305355) +refene2_old.solvent_energies(igb2=-65.003415, igb5=-64.047229) +refene2_old.dielc2_energies(igb2=-32.167520, igb5=-31.751177) +refene2_old.dielc2.solvent_energies() +refene2_old.set_pKa(9.6, deprotonated=True) + +TYR = TitratableResidue('TYR', ['N', 'H', 'CA', 'HA', 'CB', 'HB2', 'HB3', 'CG', + 'CD1', 'HD1', 'CE1', 'HE1', 'CZ', 'OH', 'HH', 'CE2', + 'HE2', 'CD2', 'HD2', 'C', 'O'], pka=9.6, typ="ph") +TYR.add_state(protcnt=1, refene=refene1, refene_old=refene1, pka_corr=9.6, # protonated + charges=[-0.4157, 0.2719, -0.0014, 0.0876, -0.0152, 0.0295, + 0.0295, -0.0011, -0.1906, 0.1699, -0.2341, 0.1656, 0.3226, + -0.5579, 0.3992, -0.2341, 0.1656, -0.1906, 0.1699, 0.5973, + -0.5679]) +TYR.add_state(protcnt=0, refene=refene2, refene_old=refene2_old, pka_corr=0.0, # deprotonated + charges=[-0.4157, 0.2719, -0.0014, 0.0876, -0.0858, 0.0190, + 0.0190, -0.2130, -0.1030, 0.1320, -0.4980, 0.1320, 0.7770, + -0.8140, 0.0, -0.4980, 0.1320, -0.1030, 0.1320, 0.5973, -0.5679]) +TYR.check() + +# Histidine +refene1 = _ReferenceEnergy(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene1.solvent_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene1.dielc2_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene1.dielc2.solvent_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene2 = _ReferenceEnergy(igb1=-4.208863, igb2=-2.84183, igb5=-2.86001, + igb7=-1.741947, igb8=-3.4000) +refene2.solvent_energies(igb2=-2.77641, igb5=-2.90517) +refene2.dielc2_energies(igb2=-1.628110, igb5=-1.691093) +refene2.dielc2.solvent_energies() +# Copying the reference energy to be printted on the old CPIN format +refene2_old = _ReferenceEnergy(igb1=-4.208863, igb2=-2.84183, igb5=-2.86001, + igb7=-1.741947, igb8=-3.4000) +refene2_old.solvent_energies(igb2=-2.77641, igb5=-2.90517) +refene2_old.dielc2_energies(igb2=-1.628110, igb5=-1.691093) +refene2_old.dielc2.solvent_energies() +refene2_old.set_pKa(6.5, deprotonated=True) +refene3 = _ReferenceEnergy(igb1=-8.230643, igb2=-6.58793, igb5=-6.70726, + igb7=-5.118453, igb8=-6.3190) +refene3.solvent_energies(igb2=-6.483630, igb5=-6.82684) +refene3.dielc2_energies(igb2=-3.444200, igb5=-3.070113) +refene3.dielc2.solvent_energies() +# Copying the reference energy to be printted on the old CPIN format +refene3_old = _ReferenceEnergy(igb1=-8.230643, igb2=-6.58793, igb5=-6.70726, + igb7=-5.118453, igb8=-6.3190) +refene3_old.solvent_energies(igb2=-6.483630, igb5=-6.82684) +refene3_old.dielc2_energies(igb2=-3.444200, igb5=-3.070113) +refene3_old.dielc2.solvent_energies() +refene3_old.set_pKa(7.1, deprotonated=True) + +HIP = TitratableResidue('HIP', ['N', 'H', 'CA', 'HA', 'CB', 'HB2', 'HB3', 'CG', + 'ND1', 'HD1', 'CE1', 'HE1', 'NE2', 'HE2', 'CD2', 'HD2', + 'C', 'O'], pka=6.6, typ="ph") +HIP.add_state(protcnt=2, refene=refene1, refene_old=refene1, pka_corr=7.1, # HIP + charges=[-0.3479, 0.2747, -0.1354, 0.1212, -0.0414, 0.0810, + 0.0810, -0.0012, -0.1513, 0.3866, -0.0170, 0.2681, -0.1718, + 0.3911, -0.1141, 0.2317, 0.7341, -0.5894]) +HIP.add_state(protcnt=1, refene=refene2, refene_old=refene2_old, pka_corr=0.6, # HID + charges=[-0.3479, 0.2747, -0.1354, 0.1212, -0.1110, 0.0402, + 0.0402, -0.0266, -0.3811, 0.3649, 0.2057, 0.1392, -0.5727, 0.0, + 0.1292, 0.1147, 0.7341, -0.5894]) +HIP.add_state(protcnt=1, refene=refene3, refene_old=refene3_old, pka_corr=0.0, # HIE + charges=[-0.3479, 0.2747, -0.1354, 0.1212, -0.1012, 0.0367, + 0.0367, 0.1868, -0.5432, 0.0, 0.1635, 0.1435, -0.2795, 0.3339, + -0.2207, 0.1862, 0.7341, -0.5894]) +HIP.check() + +# Lysine +refene1 = _ReferenceEnergy(igb2=-15.2423959, igb5=-14.5392838, igb8=-18.393654) +refene1.solvent_energies(igb2=-15.1417977, igb5=-14.3152107) +refene1.dielc2_energies(igb2=-7.239587, igb5=-6.825997) +refene1.dielc2.solvent_energies() +# Copying the reference energy to be printted on the old CPIN format +refene1_old = _ReferenceEnergy(igb2=-15.2423959, igb5=-14.5392838, igb8=-18.393654) +refene1_old.solvent_energies(igb2=-15.1417977, igb5=-14.3152107) +refene1_old.dielc2_energies(igb2=-7.239587, igb5=-6.825997) +refene1_old.dielc2.solvent_energies() +refene1_old.set_pKa(10.4, deprotonated=False) +refene2 = _ReferenceEnergy(igb2=0, igb5=0, igb8=0) +refene2.solvent_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene2.dielc2_energies(igb2=0, igb5=0, igb8=0) +refene2.dielc2.solvent_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) + +LYS = TitratableResidue('LYS', ['N', 'H', 'CA', 'HA', 'CB', 'HB2', 'HB3', 'CG', + 'HG2', 'HG3', 'CD', 'HD2', 'HD3', 'CE', 'HE2' ,'HE3', + 'NZ', 'HZ1', 'HZ2', 'HZ3', 'C', 'O'], pka=10.4, typ="ph") +LYS.add_state(protcnt=3, refene=refene1, refene_old=refene1_old, pka_corr=10.4, # protonated + charges=[-0.3479, 0.2747, -0.2400, 0.1426, -0.0094, 0.0362, + 0.0362, 0.0187, 0.0103, 0.0103, -0.0479, 0.0621, 0.0621, -0.0143, + 0.1135, 0.1135, -0.3854, 0.3400, 0.3400, 0.3400, 0.7341, -0.5894]) +LYS.add_state(protcnt=2, refene=refene2, refene_old=refene2, pka_corr=0.0, # deprotonated + charges=[-0.3479, 0.2747, -0.2400, 0.1426, -0.10961, 0.0340, + 0.0340, 0.06612, 0.01041, 0.01041, -0.03768, 0.01155, 0.01155, + 0.32604, -0.03358, -0.03358, -1.03581, 0.0, 0.38604, 0.38604, + 0.7341, -0.5894]) +LYS.check() + +# Cysteine +refene1 = _ReferenceEnergy(igb2=77.4666763, igb5=76.2588331, igb8=71.5804519) +refene1.solvent_energies(igb2=77.6041407, igb5=76.2827217) +refene1.dielc2_energies(igb2=38.090523, igb5=37.454637) +refene1.dielc2.solvent_energies(igb2=38.489170) +# Copying the reference energy to be printted on the old CPIN format +refene1_old = _ReferenceEnergy(igb2=77.4666763, igb5=76.2588331, igb8=71.5804519) +refene1_old.solvent_energies(igb2=77.6041407, igb5=76.2827217) +refene1_old.dielc2_energies(igb2=38.090523, igb5=37.454637) +refene1_old.dielc2.solvent_energies(igb2=38.489170) +refene1_old.set_pKa(8.5, deprotonated=False) +refene2 = _ReferenceEnergy(igb2=0, igb5=0, igb8=0) +refene2.solvent_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene2.dielc2_energies(igb2=0, igb5=0, igb8=0) +refene2.dielc2.solvent_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) + +CYS = TitratableResidue('CYS', ['N', 'H', 'CA', 'HA', 'CB', 'HB2', 'HB3', 'SG', + 'HG', 'C', 'O'], pka=8.5, typ="ph") +CYS.add_state(protcnt=1, refene=refene1, refene_old=refene1_old, pka_corr=8.5, # protonated + charges=[-0.4157, 0.2719, 0.0213, 0.1124, -0.1231, 0.1112, 0.1112, + -0.3119, 0.1933, 0.5973, -0.5679]) +CYS.add_state(protcnt=0, refene=refene2, refene_old=refene2, pka_corr=0.0, # deprotonated + charges=[-0.4157, 0.2719, 0.0213, 0.1124, -0.3593, 0.1122, 0.1122, + -0.8844, 0.0, 0.5973, -0.5679]) +CYS.check() + +# Deoxy-adenine +refene1 = _ReferenceEnergy(igb2=-19.8442, igb5=-19.8442) +refene1.solvent_energies() +refene1.dielc2_energies(igb2=-9.106013, igb5=-9.404867) +refene1.dielc2.solvent_energies(igb2=-9.779586) +# Copying the reference energy to be printted on the old CPIN format +refene1_old = _ReferenceEnergy(igb2=-19.8442, igb5=-19.8442) +refene1_old.solvent_energies() +refene1_old.dielc2_energies(igb2=-9.106013, igb5=-9.404867) +refene1_old.dielc2.solvent_energies(igb2=-9.779586) +refene1_old.set_pKa(3.9, deprotonated=True) +refene2 = _ReferenceEnergy(igb2=0, igb5=0, igb8=0) +refene2.solvent_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene2.dielc2_energies(igb2=0, igb5=0, igb8=0) +refene2.dielc2.solvent_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) + +DAP = TitratableResidue('DAP', ['P', 'O1P', 'O2P', "O5'", "C5'", "H5'1", "H5'2", + "C4'", "H4'", "O4'", "C1'", "H1'", 'N9', 'C8', 'H8', + 'N7', 'C5', 'C6', 'N6', 'H61', 'H62', 'N1', 'C2', 'H2', + 'N3', 'C4', "C3'", "H3'", "C2'", "H2'1", "H2'2", "O3'", + 'H1'], pka=3.9, typ="ph") +DAP.add_state(protcnt=1, refene=refene1, refene_old=refene1_old, pka_corr=0.0, # deprotonated + charges=[1.1659, -0.7761, -0.7761, -0.4954, -0.0069, 0.0754, + 0.0754, 0.1629, 0.1176, -0.3691, 0.0431, 0.1838, -0.0268, 0.1607, + 0.1877, -0.6175, 0.0725, 0.6897, -0.9123, 0.4167, 0.4167, -0.7624, + 0.5716, 0.0598, -0.7417, 0.38, 0.0713, 0.0985, -0.0854, 0.0718, + 0.0718, -0.5232, 0.0]) +DAP.add_state(protcnt=2, refene=refene2, refene_old=refene2, pka_corr=3.9, # protonated + charges=[1.1659, -0.7761, -0.7761, -0.4954, -0.0069, 0.0754, + 0.0754, 0.1629, 0.1176, -0.3691, 0.0431, 0.1838, 0.0944, 0.1617, + 0.2281, -0.5674, 0.1358, 0.5711, -0.8251, 0.4456, 0.4456, -0.575, + 0.4251, 0.1437, -0.5611, 0.3421, 0.0713, 0.0985, -0.0854, 0.0718, + 0.0718, -0.5232, 0.4301]) +DAP.check() + +# Deoxy-cytosine +refene1 = _ReferenceEnergy(igb2=-40.526, igb5=-40.526) +refene1.solvent_energies() +refene1.dielc2_energies(igb2=-19.447553, igb5=-19.842087) +refene1.dielc2.solvent_energies(igb2=-20.121129) +# Copying the reference energy to be printted on the old CPIN format +refene1_old = _ReferenceEnergy(igb2=-40.526, igb5=-40.526) +refene1_old.solvent_energies() +refene1_old.dielc2_energies(igb2=-19.447553, igb5=-19.842087) +refene1_old.dielc2.solvent_energies(igb2=-20.121129) +refene1_old.set_pKa(4.3, deprotonated=True) +refene2 = _ReferenceEnergy(igb2=0, igb5=0) +refene2.solvent_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene2.dielc2_energies(igb2=0, igb5=0, igb8=0) +refene2.dielc2.solvent_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) + +DCP = TitratableResidue('DCP', ['P', 'O1P', 'O2P', "O5'", "C5'", "H5'1", "H5'2", + "C4'", "H4'", "O4'", "C1'", "H1'", 'N1', 'C6', 'H6', + 'C5', 'H5', 'C4', 'N4', 'H41', 'H42', 'N3', 'C2', 'O2', + "C3'", "H3'", "C2'", "H2'1", "H2'2", "O3'", 'H3'], + pka=4.3, typ="ph") +DCP.add_state(protcnt=1, refene=refene1, refene_old=refene1_old, pka_corr=0.0, # deprotonated + charges=[1.1659, -0.7761, -0.7761, -0.4954, -0.0069, 0.0754, + 0.0754, 0.1629, 0.1176, -0.3691, -0.0116, 0.1963, -0.0339, + -0.0183, 0.2293, -0.5222, 0.1863, 0.8439, -0.9773, 0.4314, 0.4314, + -0.7748, 0.7959, -0.6548, 0.0713, 0.0985, -0.0854, 0.0718, 0.0718, + -0.5232, 0.0]) +DCP.add_state(protcnt=2, refene=refene2, refene_old=refene2, pka_corr=4.3, # protonated + charges=[1.1659, -0.7761, -0.7761, -0.4954, -0.0069, 0.0754, + 0.0754, 0.1629, 0.1176, -0.3691, -0.0116, 0.1963, 0.2167, -0.0282, + 0.2713, -0.4162, 0.2179, 0.6653, -0.859, 0.4598, 0.4598, -0.4956, + 0.5371, -0.5028, 0.0713, 0.0985, -0.0854, 0.0718, 0.0718, -0.5232, + 0.4108]) +DCP.check() + +# Deoxy-guanine +refene1 = _ReferenceEnergy(igb2=0, igb5=0, igb8=0) +refene1.solvent_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene1.dielc2_energies(igb2=0, igb5=0, igb8=0) +refene1.dielc2.solvent_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene2 = _ReferenceEnergy(igb2=-90.0011, igb5=-90.0011) +refene2.solvent_energies() +refene2.dielc2_energies(igb2=-44.031593, igb5=-43.588343) +refene2.dielc2.solvent_energies(igb2=-45.090067) +# Copying the reference energy to be printted on the old CPIN format +refene2_old = _ReferenceEnergy(igb2=-90.0011, igb5=-90.0011) +refene2_old.solvent_energies() +refene2_old.dielc2_energies(igb2=-44.031593, igb5=-43.588343) +refene2_old.dielc2.solvent_energies(igb2=-45.090067) +refene2_old.set_pKa(9.2, deprotonated=True) + +DG = TitratableResidue('DG', ['P', 'O1P', 'O2P', "O5'", "C5'", "H5'1", "H5'2", + "C4'", "H4'", "O4'", "C1'", "H1'", 'N9', 'C8', 'H8', + 'N7', 'C5', 'C6', 'O6', 'N1', 'H1', 'C2', 'N2', 'H21', + 'H22', 'N3', 'C4', "C3'", "H3'", "C2'", "H2'1", "H2'2", + "O3'"], pka=9.2, typ="ph") +DG.add_state(protcnt=1, refene=refene1, refene_old=refene1, pka_corr=9.2, # protonated + charges=[1.1659, -0.7761, -0.7761, -0.4954, -0.0069, 0.0754, + 0.0754, 0.1629, 0.1176, -0.3691, 0.0358, 0.1746, 0.0577, 0.0736, + 0.1997, -0.5725, 0.1991, 0.4918, -0.5699, -0.5053, 0.352, 0.7432, + -0.923, 0.4235, 0.4235, -0.6636, 0.1814, 0.0713, 0.0985, -0.0854, + 0.0718, 0.0718, -0.5232]) +DG.add_state(protcnt=0, refene=refene2, refene_old=refene2_old, pka_corr=0.0, # deprotonated + charges=[1.1659, -0.7761, -0.7761, -0.4954, -0.0069, 0.0754, + 0.0754, 0.1629, 0.1176, -0.3691, 0.0358, 0.1746, -0.0507, 0.0779, + 0.1516, -0.6122, 0.0806, 0.7105, -0.7253, -0.8527, 0.0, 0.9561, + -0.9903, 0.3837, 0.3837, -0.8545, 0.2528, 0.0713, 0.0985, -0.0854, + 0.0718, 0.0718, -0.5232]) +DG.check() + +# Deoxy-thymine +refene1 = _ReferenceEnergy(igb2=0, igb5=0) +refene1.solvent_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene1.dielc2_energies(igb2=0, igb5=0, igb8=0) +refene1.dielc2.solvent_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene2 = _ReferenceEnergy(igb2=-56.7729, igb5=-56.7729) +refene2.solvent_energies(igb2=-28.429391) +refene2.dielc2_energies(igb2=-28.085730, igb5=-27.298290) +refene2.dielc2.solvent_energies() +# Copying the reference energy to be printted on the old CPIN format +refene2_old = _ReferenceEnergy(igb2=-56.7729, igb5=-56.7729) +refene2_old.solvent_energies(igb2=-28.429391) +refene2_old.dielc2_energies(igb2=-28.085730, igb5=-27.298290) +refene2_old.dielc2.solvent_energies() +refene2_old.set_pKa(9.7, deprotonated=True) + +DT = TitratableResidue('DT', ['P', 'O1P', 'O2P', "O5'", "C5'", "H5'1", "H5'2", + "C4'", "H4'", "O4'", "C1'", "H1'", 'N1', 'C6', 'H6', + 'C5', 'C7', 'H71', 'H72', 'H73', 'C4', 'O4', 'N3', 'H3', + 'C2', 'O2', "C3'", "H3'", "C2'", "H2'1", "H2'2", "O3'"], + pka=9.7, typ="ph") +DT.add_state(protcnt=1, refene=refene1, refene_old=refene1, pka_corr=9.7, # protonated + charges=[1.1659, -0.7761, -0.7761, -0.4954, -0.0069, 0.0754, + 0.0754, 0.1629, 0.1176, -0.3691, 0.068, 0.1804, -0.0239, -0.2209, + 0.2607, 0.0025, -0.2269, 0.077, 0.077, 0.077, 0.5194, -0.5563, + -0.434, 0.342, 0.5677, -0.5881, 0.0713, 0.0985, -0.0854, 0.0718, + 0.0718, -0.5232]) +DT.add_state(protcnt=0, refene=refene2, refene_old=refene2_old, pka_corr=0.0, # deprotonated + charges=[1.1659, -0.7761, -0.7761, -0.4954, -0.0069, 0.0754, + 0.0754, 0.1629, 0.1176, -0.3691, 0.068, 0.1804, -0.2861, -0.1874, + 0.2251, -0.1092, -0.2602, 0.0589, 0.0589, 0.0589, 0.8263, -0.7396, + -0.9169, 0.0, 0.9167, -0.7722, 0.0713, 0.0985, -0.0854, 0.0718, + 0.0718, -0.5232]) +DT.check() + +# Adenine +refene1 = _ReferenceEnergy(igb2=0, igb5=0) +refene1.solvent_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene1.dielc2_energies(igb2=0, igb5=0, igb8=0) +refene1.dielc2.solvent_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene2 = _ReferenceEnergy(igb2=14.851840012, igb5=15.1166001033) +refene2.solvent_energies(igb2=15.026098360790293,igb5=15.143651997474915) +refene2.solvent_energies() +refene2.dielc2_energies(igb2=6.953887, igb5=7.092043) +refene2.dielc2.solvent_energies(igb2=7.544988) +# Copying the reference energy to be printted on the old CPIN format +refene2_old = _ReferenceEnergy(igb2=14.851840012, igb5=15.1166001033) +refene2_old.solvent_energies(igb2=15.026098360790293,igb5=15.143651997474915) +refene2_old.solvent_energies() +refene2_old.dielc2_energies(igb2=6.953887, igb5=7.092043) +refene2_old.dielc2.solvent_energies(igb2=7.544988) +refene2_old.set_pKa(3.5, deprotonated=False) + +AP = TitratableResidue('AP', ['P', 'O1P', 'O2P', "O5'", "C5'", "H5'1", "H5'2", + "C4'", "H4'", "O4'", "C1'", "H1'", 'N9', 'C8', 'H8', + 'N7', 'C5', 'C6', 'N6', 'H61', 'H62', 'N1', 'C2', 'H2', + 'N3', 'C4', "C3'", "H3'", "C2'", "H2'1", "O2'", "HO'2", + "O3'", 'H1'], pka=3.9, typ="ph") +AP.add_state(protcnt=0, refene=refene1, refene_old=refene1, pka_corr=0.0, # deprotonated + charges=[1.1662, -0.776, -0.776, -0.4989, 0.0558, 0.0679, 0.0679, + 0.1065, 0.1174, -0.3548, 0.0394, 0.2007, -0.0251, 0.2006, 0.1553, + -0.6073, 0.0515, 0.7009, -0.9019, 0.4115, 0.4115, -0.7615, 0.5875, + 0.0473, -0.6997, 0.3053, 0.2022, 0.0615, 0.067, 0.0972, -0.6139, + 0.4186, -0.5246, 0.0]) +AP.add_state(protcnt=1, refene=refene2, refene_old=refene2_old, pka_corr=3.5, # protonated + charges=[1.1662, -0.776, -0.776, -0.4989, 0.0558, 0.0679, 0.0679, + 0.1065, 0.1174, -0.3548, 0.0394, 0.2007, 0.0961, 0.2011, 0.1965, + -0.5569, 0.1136, 0.5845, -0.8152, 0.4403, 0.4403, -0.5776, 0.4435, + 0.1307, -0.5201, 0.2681, 0.2022, 0.0615, 0.067, 0.0972, -0.6139, + 0.4186, -0.5246, 0.431]) +AP.check() + +# Cytosine +refene1 = _ReferenceEnergy(igb2=0, igb5=0) +refene1.solvent_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene1.dielc2_energies(igb2=0, igb5=0, igb8=0) +refene1.dielc2.solvent_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene2 = _ReferenceEnergy(igb2=37.501800178, igb5=38.0081251132) +refene2.solvent_energies(igb2=37.378544257354164, igb5=37.90444570773976) +refene2.dielc2_energies(igb2=18.483513, igb5=19.016390) +refene2.dielc2.solvent_energies() +# Copying the reference energy to be printted on the old CPIN format +refene2_old = _ReferenceEnergy(igb2=37.501800178, igb5=38.0081251132) +refene2_old.solvent_energies(igb2=37.378544257354164, igb5=37.90444570773976) +refene2_old.dielc2_energies(igb2=18.483513, igb5=19.016390) +refene2_old.dielc2.solvent_energies() +refene2_old.set_pKa(4.2, deprotonated=False) + +CP = TitratableResidue('CP', ['P', 'O1P', 'O2P', "O5'", "C5'", "H5'1", "H5'2", + "C4'", "H4'", "O4'", "C1'", "H1'", 'N1', 'C6', 'H6', + 'C5', 'H5', 'C4', 'N4', 'H41', 'H42', 'N3', 'C2', 'O2', + "C3'", "H3'", "C2'", "H2'1", "O2'", "HO'2", "O3'", 'H3'], + pka=4.3, typ="ph") +CP.add_state(protcnt=1, refene=refene1, refene_old=refene1, pka_corr=0.0, # deprotonated + charges=[1.1662, -0.776, -0.776, -0.4989, 0.0558, 0.0679, 0.0679, + 0.1065, 0.1174, -0.3548, 0.0066, 0.2029, -0.0484, 0.0053, 0.1958, + -0.5215, 0.1928, 0.8185, -0.953, 0.4234, 0.4234, -0.7584, 0.7538, + -0.6252, 0.2022, 0.0615, 0.067, 0.0972, -0.6139, 0.4186, -0.5246, + 0.0]) +CP.add_state(protcnt=2, refene=refene2, refene_old=refene2_old, pka_corr=4.2, # protonated + charges=[1.1662, -0.776, -0.776, -0.4989, 0.0558, 0.0679, 0.0679, + 0.1065, 0.1174, -0.3548, 0.0066, 0.2029, 0.1954, 0.0028, 0.2366, + -0.4218, 0.2253, 0.6466, -0.8363, 0.4518, 0.4518, -0.4871, 0.5039, + -0.4753, 0.2022, 0.0615, 0.067, 0.0972, -0.6139, 0.4186, -0.5246, + 0.4128]) +CP.check() + +# Guanine +refene1 = _ReferenceEnergy(igb2=0, igb5=0) +refene1.solvent_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene1.dielc2_energies(igb2=0, igb5=0, igb8=0) +refene1.dielc2.solvent_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene2 = _ReferenceEnergy(igb2=-97.094725165, igb5=-96.0365352027) +refene2.solvent_energies(igb2=-97.31657849010276, igb5=-95.95654436492156) +refene2.dielc2_energies(igb2=-47.410980, igb5=-47.008233) +refene2.dielc2.solvent_energies(igb2=-48.222021) +# Copying the reference energy to be printted on the old CPIN format +refene2_old = _ReferenceEnergy(igb2=-97.094725165, igb5=-96.0365352027) +refene2_old.solvent_energies(igb2=-97.31657849010276, igb5=-95.95654436492156) +refene2_old.dielc2_energies(igb2=-47.410980, igb5=-47.008233) +refene2_old.dielc2.solvent_energies(igb2=-48.222021) +refene2_old.set_pKa(9.2, deprotonated=True) + +G = TitratableResidue('G', ['P', 'O1P', 'O2P', "O5'", "C5'", "H5'1", "H5'2", + "C4'", "H4'", "O4'", "C1'", "H1'", 'N9', 'C8', 'H8', 'N7', + 'C5', 'C6', 'O6', 'N1', 'H1', 'C2', 'N2', 'H21', 'H22', + 'N3', 'C4', "C3'", "H3'", "C2'", "H2'1", "O2'", "HO'2", + "O3'"], pka=9.2, typ="ph") +G.add_state(protcnt=1, refene=refene1, refene_old=refene1, pka_corr=9.2, # protonated + charges=[1.1662, -0.776, -0.776, -0.4989, 0.0558, 0.0679, 0.0679, + 0.1065, 0.1174, -0.3548, 0.0191, 0.2006, 0.0492, 0.1374, 0.164, + -0.5709, 0.1744, 0.477, -0.5597, -0.4787, 0.3424, 0.7657, -0.9672, + 0.4364, 0.4364, -0.6323, 0.1222, 0.2022, 0.0615, 0.067, 0.0972, + -0.6139, 0.4186, -0.5246]) +G.add_state(protcnt=0, refene=refene2, refene_old=refene2_old, pka_corr=0.0, # deprotonated + charges=[1.1662, -0.776, -0.776, -0.4989, 0.0558, 0.0679, 0.0679, + 0.1065, 0.1174, -0.3548, 0.0191, 0.2006, -0.0623, 0.1479, 0.1137, + -0.6127, 0.0488, 0.7137, -0.7191, -0.8557, 0.0, 0.9976, -1.0387, + 0.3969, 0.3969, -0.8299, 0.1992, 0.2022, 0.0615, 0.067, 0.0972, + -0.6139, 0.4186, -0.5246]) +G.check() + +# Uracil +refene1 = _ReferenceEnergy(igb2=0, igb5=0) +refene1.solvent_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene1.dielc2_energies(igb2=0, igb5=0, igb8=0) +refene1.dielc2.solvent_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene2 = _ReferenceEnergy(igb2=-136.326020191, igb5=-134.938275039) +refene2.solvent_energies(igb2=-136.5653533428478, igb5=-135.06973320905044) +refene2.dielc2_energies(igb2=-67.270690, igb5=-66.605330) +refene2.dielc2.solvent_energies() +# Copying the reference energy to be printted on the old CPIN format +refene2_old = _ReferenceEnergy(igb2=-136.326020191, igb5=-134.938275039) +refene2_old.solvent_energies(igb2=-136.5653533428478, igb5=-135.06973320905044) +refene2_old.dielc2_energies(igb2=-67.270690, igb5=-66.605330) +refene2_old.dielc2.solvent_energies() +refene2_old.set_pKa(9.2, deprotonated=True) + +U = TitratableResidue('U', ['P', 'O1P', 'O2P', "O5'", "C5'", "H5'1", "H5'2", + "C4'", "H4'", "O4'", "C1'", "H1'", 'N1', 'C6', 'H6', 'C5', + 'H5', 'C4', 'O4', 'N3', 'H3', 'C2', 'O2', "C3'", "H3'", + "C2'", "H2'1", "O2'", "HO'2", "O3'"], pka=9.3, typ="ph") +U.add_state(protcnt=1, refene=refene1, refene_old=refene1, pka_corr=9.2, # protonated + charges=[1.1662, -0.776, -0.776, -0.4989, 0.0558, 0.0679, 0.0679, + 0.1065, 0.1174, -0.3548, 0.0674, 0.1824, 0.0418, -0.1126, 0.2188, + -0.3635, 0.1811, 0.5952, -0.5761, -0.3549, 0.3154, 0.4687, -0.5477, + 0.2022, 0.0615, 0.067, 0.0972, -0.6139, 0.4186, -0.5246]) +U.add_state(protcnt=0, refene=refene2, refene_old=refene2_old, pka_corr=0.0, # deprotonated + charges=[1.1662, -0.776, -0.776, -0.4989, 0.0558, 0.0679, 0.0679, + 0.1065, 0.1174, -0.3548, 0.0674, 0.1824, -0.2733, 0.0264, 0.1501, + -0.582, 0.156, 0.9762, -0.7808, -0.9327, 0.0, 0.8698, -0.7435, + 0.2022, 0.0615, 0.067, 0.0972, -0.6139, 0.4186, -0.5246]) +U.check() + +# HEH: HEME ring + parts of 2 HIS + 2 CYS +refene1 = _ReferenceEnergy(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene1.solvent_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene1.dielc2_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene1.dielc2.solvent_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene2 = _ReferenceEnergy(igb2=-15.493731, igb5=-16.349152, igb7=-16.509509, igb8=-22.025653) # Implicit +refene2.solvent_energies(igb2=-15.209270, igb5=-15.840853, igb7=-15.495868) # Explicit +refene2.dielc2_energies() +refene2.dielc2.solvent_energies() + +HEH = TitratableResidue('HEH', ['FE', 'NA', 'C1A', 'C2A', 'C3A', 'CMA', 'HMA1', 'HMA2', 'HMA3', 'C4A', + 'CHB', 'HHB', 'C1B', 'NB', 'C2B', 'CMB', 'HMB1', 'HMB2', 'HMB3', 'C3B', + 'CAB', 'HAB', 'CBB', 'HBB1', 'HBB2', 'HBB3', 'C4B', 'CHC', 'HHC', 'C1C', + 'NC', 'C2C', 'CMC', 'HMC1', 'HMC2', 'HMC3', 'C3C', 'CAC', 'HAC', 'CBC', + 'HBC1', 'HBC2', 'HBC3', 'C4C', 'CHD', 'HHD', 'C1D', 'ND', 'C2D', 'CMD', + 'HMD1', 'HMD2', 'HMD3', 'C3D', 'C4D', 'CHA', 'HHA', 'CBC1', 'HB2C', + 'HB3C', 'SGC1', 'CB1', 'HB21', 'HB31', 'CG1', 'ND11', 'HD11', 'CE11', + 'HE11', 'NE21', 'CD21', 'HD21', 'CBB2', 'HB2B', 'HB3B', 'SGB2', 'CB2', + 'HB22', 'HB32', 'CG2', 'ND12', 'HD12', 'CE12', 'HE12', 'NE22', 'CD22', + 'HD22'], + eo=-0.203, typ="redox") +HEH.add_state(eleccnt=2, refene=refene1, eo_corr=0.0, # FE3+ (oxidized state) + charges=[ 0.6660, -0.1530, -0.0956, 0.1274, 0.1624, -0.2600, 0.0743, 0.0743, 0.0743, + -0.0766, -0.0586, 0.1300, -0.0206, -0.2560, 0.1394, -0.2240, 0.0663, 0.0663, + 0.0663, 0.0734, -0.0935, 0.1765, -0.4035, 0.1375, 0.1375, 0.1375, 0.0504, + -0.1806, 0.1430, 0.0364, -0.2390, 0.1784, -0.2325, 0.0635, 0.0635, 0.0635, + 0.0084, -0.0570, 0.2130, -0.4090, 0.1357, 0.1357, 0.1357, -0.0266, -0.0576, + 0.1360, -0.1156, -0.1130, 0.1604, -0.2575, 0.0752, 0.0752, 0.0752, 0.1444, + -0.1126, 0.0104, 0.1090, -0.3349, 0.1297, 0.1297, -0.1760, -0.0803, 0.0269, + 0.0269, 0.1990, -0.2930, 0.3650, 0.0120, 0.1180, -0.0400, -0.2020, 0.1790, + -0.3349, 0.1297, 0.1297, -0.1760, -0.0803, 0.0269, 0.0269, 0.1990, -0.2930, + 0.3650, 0.0120, 0.1180, -0.0400, -0.2020, 0.1790] + ) +HEH.add_state(eleccnt=3, refene=refene2, eo_corr=-0.203, # FE2+ (reduced state) + charges=[ 0.4800, -0.1337, -0.1455, 0.1285, 0.1325, -0.2545, 0.0608, 0.0608, 0.0608, + -0.0865, -0.0815, 0.1220, -0.0425, -0.2490, 0.1605, -0.1935, 0.0422, 0.0422, + 0.0422, -0.0025, -0.0390, 0.1720, -0.4255, 0.1348, 0.1348, 0.1348, 0.0405, + -0.1725, 0.1320, -0.0525, -0.1980, 0.2125, -0.1985, 0.0405, 0.0405, 0.0405, + -0.0355, -0.0195, 0.1915, -0.4340, 0.1320, 0.1320, 0.1320, -0.0275, -0.0805, + 0.1290, -0.1275, -0.1020, 0.1335, -0.2525, 0.0615, 0.0615, 0.0615, 0.1415, + -0.1575, 0.0275, 0.0950, -0.3343, 0.1300, 0.1300, -0.2315, -0.0967, 0.0187, + 0.0187, 0.2030, -0.3450, 0.3550, 0.0110, 0.1090, -0.0270, -0.2170, 0.1750, + -0.3343, 0.1300, 0.1300, -0.2315, -0.0967, 0.0187, 0.0187, 0.2030, -0.3450, + 0.3550, 0.0110, 0.1090, -0.0270, -0.2170, 0.1750] + ) +HEH.check() + +# Propionate +refene1 = _ReferenceEnergy(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene1.solvent_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene1.dielc2_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene1.dielc2.solvent_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene2 = _ReferenceEnergy(igb2=10.356928, igb5=9.943308, igb7=7.020632, igb8=5.259028) # Implicit +refene2.solvent_energies(igb2=16.751825, igb5=15.661934, igb7=12.906876, igb8=15.024553) # Explicit +refene2.dielc2_energies() +refene2.dielc2.solvent_energies() +# Copying the reference energy to be printted on the old CPIN format +refene2_old = _ReferenceEnergy(igb2=10.356928, igb5=9.943308, igb7=7.020632, igb8=5.259028) # Implicit +refene2_old.solvent_energies(igb2=16.751825, igb5=15.661934, igb7=12.906876, igb8=15.024553) # Explicit +refene2_old.dielc2_energies() +refene2_old.dielc2.solvent_energies() +refene2_old.set_pKa(4.85, deprotonated=False) + +PRN = TitratableResidue('PRN', + ['CA', 'HA1', 'HA2', 'CB', 'HB1', 'HB2', 'CG', + 'O1', 'O2', 'H11', 'H12', 'H21', 'H22'], pka=4.85, typ="ph") + +PRN.add_state(protcnt=0, refene=refene1, refene_old=refene1, pka_corr=0.0, # deprotonated + charges=[-0.0508, -0.0173, + -0.0173, 0.0026, -0.0425, -0.0425, 0.8054, -0.8188, -0.8188, 0.0, + 0.0, 0.0, 0.0]) + +PRN.add_state(protcnt=1, refene=refene2, refene_old=refene2_old, pka_corr=4.85, # protonated syn-O1 + charges=[-0.0181, 0.0256, 0.0256, + -0.0284, 0.0430, 0.0430, 0.6801, -0.6511, -0.5838, 0.4641, + 0.0, 0.0, 0.0]) + +PRN.add_state(protcnt=1, refene=refene2, refene_old=refene2_old, pka_corr=4.85, # protonated anti-O1 + charges=[-0.0181, 0.0256, 0.0256, + -0.0284, 0.0430, 0.0430, 0.6801, -0.6511, -0.5838, 0.0, + 0.4641, 0.0, 0.0]) + +PRN.add_state(protcnt=1, refene=refene2, refene_old=refene2_old, pka_corr=4.85, # protonated syn-O2 + charges=[-0.0181, 0.0256, 0.0256, + -0.0284, 0.0430, 0.0430, 0.6801, -0.5838, -0.6511, 0.0, + 0.0, 0.4641, 0.0]) + +PRN.add_state(protcnt=1, refene=refene2, refene_old=refene2_old, pka_corr=4.85, # protonated anti-O2 + charges=[-0.0181, 0.0256, 0.0256, + -0.0284, 0.0430, 0.0430, 0.6801, -0.5838, -0.6511, 0.0, + 0.0, 0.0, 0.4641]) + +PRN.check() + +# Tyrosine, pH and redox active +refene1 = _ReferenceEnergy(igb2=0, igb5=0, igb8=0) +refene1.solvent_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene1.dielc2_energies(igb2=0, igb5=0, igb8=0) +refene1.dielc2.solvent_energies(igb1=0, igb2=0, igb5=0, igb7=0, igb8=0) +refene2 = _ReferenceEnergy(igb2=5.409084) # Implicit +refene2.solvent_energies(igb2=5.210075) # Explicit +refene2.dielc2_energies() +refene2.dielc2.solvent_energies() +refene3 = _ReferenceEnergy(igb2=-52.293859) # Implicit +refene3.solvent_energies(igb2=-52.185005) # Explicit +refene3.dielc2_energies() +refene3.dielc2.solvent_energies() +refene4 = _ReferenceEnergy(igb2=24.166408) # Implicit +refene4.solvent_energies(igb2=24.224913) # Explicit +refene4.dielc2_energies() +refene4.dielc2.solvent_energies() + +TYX = TitratableResidue('TYX', ['N', 'H', 'CA', 'HA', 'CB', 'HB2', 'HB3', 'CG', + 'CD1', 'HD1', 'CE1', 'HE1', 'CZ', 'OH', 'HH', 'CE2', + 'HE2', 'CD2', 'HD2', 'C', 'O'], typ="phredox") +# Note: pka_corr differences only make sense between states with the same number of electrons +# Note: eo_corr differences only make sense between states with the same number of protons +TYX.add_state(protcnt=1, eleccnt=1, refene=refene1, pka_corr=9.6, eo_corr=1.4, # tyrOH + charges=[-0.4157, 0.2719, -0.0014, 0.0876, -0.1163, 0.0548, + 0.0548, -0.0139, -0.1142, 0.1615, -0.3410, 0.1911, + 0.4198, -0.5278, 0.3621, -0.3410, 0.1911, -0.1142, + 0.1615, 0.5973, -0.5679]) +TYX.add_state(protcnt=1, eleccnt=0, refene=refene2, pka_corr=-2.0, eo_corr=0.0, # tyrOH+ + charges=[-0.4157, 0.2719, -0.0014, 0.0876, -0.2911, 0.1123, + 0.1123, 0.4479, -0.1923, 0.2177, -0.1736, 0.2109, + 0.5560, -0.4376, 0.4031, -0.1736, 0.2109, -0.1923, + 0.2177, 0.5973, -0.5679]) +TYX.add_state(protcnt=0, eleccnt=1, refene=refene3, pka_corr=0.0, eo_corr=0.71, # tyrO- + charges=[-0.4157, 0.2719, -0.0014, 0.0876, -0.0500, 0.0300, + 0.0300, -0.1786, -0.1545, 0.1418, -0.4793, 0.1318, + 0.7197, -0.8026, 0.0000, -0.4793, 0.1318, -0.1545, + 0.1418, 0.5974, -0.5678]) +TYX.add_state(protcnt=0, eleccnt=0, refene=refene4, pka_corr=0.0, eo_corr=0.0, # tyrO + charges=[-0.4157, 0.2719, -0.0014, 0.0876, -0.1941, 0.0931, + 0.0931, 0.0538, -0.1216, 0.1629, -0.3224, 0.1643, + 0.6679, -0.4519, 0.0000, -0.3224, 0.1643, -0.1216, + 0.1629, 0.5973, -0.5679]) +TYX.check() diff --git a/pmx/workflow/parmed/charmm/__init__.py b/pmx/workflow/parmed/charmm/__init__.py new file mode 100644 index 00000000..460d6d1f --- /dev/null +++ b/pmx/workflow/parmed/charmm/__init__.py @@ -0,0 +1,31 @@ +""" +This package contains code for reading CHARMM structure files for setting up a +simulation with CHARMM; specifically PSF, PAR, RTF, and STR files + + - PAR : Parameter file (PRM) -- this contains all of the force field + parameters (e.g., bond lengths and strengths) for all of the atom + types + + - RTF : Residue Topology File -- this file contains the residue + connectivity tables as well as a definition of all of the atom + types. Also contains an internal coordinate representation of the + residues + + - PSF : Protein Structure File -- this is the main file type in CHARMM + simulations that defines all of the residues in a system as well as + the atom types and connectivity between the atoms + + - STR : Stream file -- Source of additional information and CHARMM commands + that can contain RTF and PAR information. Allows users to define + additional parameters without 'contaminating' the original force + field parameter files +""" + +__authors__ = 'Jason Swails' +__contributors__ = 'Jason Deckman' +__all__ = ['psf', 'parameters', 'charmmcrds', 'CharmmPsfFile', + 'CharmmParameterSet', 'CharmmCrdFile', 'CharmmRstFile'] + +from .psf import CharmmPsfFile +from .parameters import CharmmParameterSet +from .charmmcrds import CharmmCrdFile, CharmmRstFile diff --git a/pmx/workflow/parmed/charmm/_charmmfile.py b/pmx/workflow/parmed/charmm/_charmmfile.py new file mode 100644 index 00000000..bdd51374 --- /dev/null +++ b/pmx/workflow/parmed/charmm/_charmmfile.py @@ -0,0 +1,186 @@ +""" +Provides a class for reading CHARMM-style files. The key component to these +files is that the ! character is a comment character and everything after ! is +ignored. +""" +from ..utils.io import genopen + +class CharmmFile: + """ + A CHARMM file that recognizes the "!" character as a 'comment' token. It + can be iterated over and generally treated like a file object, but only + spits out strings that have been truncated at its first comment character. + + There is currently no way to recognize a ! as a _non_ comment character, + since allowing an escape character does not seem to be common practice and + would likely introduce negative performance implications. + """ + + def __init__(self, fname, mode='r'): + if mode not in ('r', 'w'): + raise ValueError('Cannot open CharmmFile with mode "%s"' % mode) + if mode == 'r': + self.status = 'OLD' + else: + self.status = 'NEW' + self._handle = genopen(fname, mode) + self.closed = False + self.line_number = 0 + self.comment = '' + + def __enter__(self): + self._handle.__enter__() + return self + + def __exit__(self, *args): + if not self.closed: + self.close() + + def tell(self): + return self._handle.tell() + + def seek(self, value): + return self._handle.seek(value) + + def write(self, *args, **kwargs): + return self._handle.write(*args, **kwargs) + + def __iter__(self): + # Iterate over the file + parts = [] + for line in self._handle: + try: + idx = line.index('!') + except ValueError: + # There is no comment... + idx = None + end = '' + self.comment = '' + if line.rstrip('\r\n').endswith('-'): + # Continuation + parts.append(line.rstrip('\r\n')[:-1]) # Skip the continuation character + continue + else: + # Lines with no comment cannot continue + end = '\n' + self.comment = line[idx:].rstrip() + parts.append(line[:idx] + end) + yield ' '.join(parts) + # Reset parts + parts = [] + + def readline(self): + self.line_number += 1 + line = self._handle.readline() + parts = [] + while line: + if line.rstrip('\r\n').endswith('-'): + # Continuation + parts.append(line.rstrip('\r\n')[:-1]) # Skip the continuation character + line = self._handle.readline() + self.line_number += 1 + else: + parts.append(line) + break # done with this line + line = ' '.join(parts) + try: + idx = line.index('!') + self.comment = line[idx:].rstrip() + end = '\n' + except ValueError: + idx = None + end = '' + self.comment = '' + return line[:idx] + end + + def readlines(self): + return [line for line in self] + + def read(self): + return ''.join(self.readlines()) + + def close(self): + self._handle.close() + self.closed = True + + def rewind(self): + """ Return to the beginning of the file """ + self._handle.seek(0) + + def __del__(self): + try: + self.closed or self._handle.close() + except AttributeError: + # It didn't make it out of the constructor + pass + +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +class CharmmStreamFile(object): + """ + The stream file is broken down into sections of commands delimited by the + strings: + read
+ .... + .... + end + This object provides iterators over those sections and a file-like API for + dealing with the text. + + """ + def __init__(self, fname): + self.lines = [] + self.comments = [] + with CharmmFile(fname, 'r') as f: + for line in f: + self.lines.append(line) + self.comments.append(f.comment) + self.line_number = 0 + + def __iter__(self): + return iter(self.lines) + + def rewind(self): + """ Return to the beginning of the file """ + self.line_number = 0 + + def next_section(self): + """ + Fast-forwards the file to the next CHARMM command section + + Returns + ------- + name, data, comments : str, list of str, list of str + name is the line defining the section that's being returned, whereas + data is a list of all lines in the section, and comments is a list + of all comments (same size as data) for all those lines + + Notes + ----- + The line pointer will be set to the line defining the section + """ + lines = [] + comments = [] + while self.line_number < len(self.lines): + line = self.lines[self.line_number].strip() + comment = self.comments[self.line_number].strip() + if line[:4].lower() == 'read': + title = line.strip() + self.line_number += 1 + line = self.lines[self.line_number] + while line and not line.strip().lower().startswith('end'): + lines.append(line) + comments.append(comment) + self.line_number += 1 + line = self.lines[self.line_number] + comment = self.comments[self.line_number] + if line[:3].upper() == 'END': + lines.append(line) + comments.append(comment) + return title, lines, comments + self.line_number += 1 + # No sections left + return None, None, None + + def __del__(self): + pass diff --git a/pmx/workflow/parmed/charmm/charmmcrds.py b/pmx/workflow/parmed/charmm/charmmcrds.py new file mode 100644 index 00000000..3ab2382a --- /dev/null +++ b/pmx/workflow/parmed/charmm/charmmcrds.py @@ -0,0 +1,369 @@ +""" +Provides a class for parsing CHARMM-style coordinate files, namely CHARMM .crd +(coordinate) files and CHARMM .rst (restart) file. Uses CharmmFile class in +_charmmfile.py for reading files + +Author: Jason Deckman +Contributors: Jason Swails +Date: June 19, 2015 +""" +from contextlib import closing +import numpy as np +from ..utils import io +from ..formats.registry import FileFormatType +from ..exceptions import CharmmError +from .. import unit as u +from ..vec3 import Vec3 + +CHARMLEN = 22 +TIMESCALE = 4.888821E-14 * 1e12 # AKMA time units to picoseconds +ONE_TIMESCALE = 1 / TIMESCALE + +class CharmmCrdFile(metaclass=FileFormatType): + """ + Reads and parses a CHARMM coordinate file (.crd) into its components, + namely the coordinates, CHARMM atom types, resid, resname, etc. + + Parameters + ---------- + fname : str + Name of the restart file to parse + + Attributes + ---------- + natom : int + Number of atoms in the system + resname : list of str + List of all residue names in the system + coordinates : np.ndarray with shape (1, natom, 3) + Atomic coordinates in a numpy array + positions : natom x 3 distance Quantity + 2-D list of all coordinates with the appropriate distance unit attached. + Has the format [ [x1, y1, z1], [x2, y2, z2], ... ] + """ + + @staticmethod + def id_format(filename): + """ Identifies the file type as a CHARMM coordinate file + + Parameters + ---------- + filename : str + Name of the file to check format for + + Returns + ------- + is_fmt : bool + True if it is a CHARMM coordinate file + """ + with closing(io.genopen(filename)) as f: + line = f.readline() + while line and len(line.strip()) == 0: # Skip whitespace + line = f.readline() + + intitle = True + while intitle: + line = f.readline() + if len(line.strip()) == 0: + intitle = False + elif line[0] != '*': + intitle = False + else: + intitle = True + + while line and len(line.strip()) == 0: # Skip whitespace + line = f.readline() + + try: + natom = int(line.split()[0]) + for row in range(min(natom, 3)): + line = f.readline().split() + int(line[0]) + int(line[1]) + float(line[4]) + float(line[5]) + float(line[6]) + float(line[9]) + except (IndexError, ValueError): + return False + + return True + + def __init__(self, fname): + self.atomno = [] # Atom number + self.resno = [] # Residue number + self.resname = [] # Residue name + self.resid = [] # Residue ID + self.atname = [] # Atom type + self.coords = [] # 3N atomic coordinates + self.title = [] # .crd file title block + self.segid = [] # Segment ID + self.weighting = [] # Atom weighting + + self.natom = 0 # Number of atoms specified in file + self._parse(fname) + + @property + def positions(self): + """ + Atomic coordinates with units attached to them with the shape (natom, 3) + """ + return [Vec3(*xyz) for xyz in self.coordinates[0]] * u.angstroms + + @property + def coordinates(self): + return self.coords + + @property + def box(self): + return None + + def _parse(self, fname): + + with closing(io.genopen(fname, 'r')) as crdfile: + line = crdfile.readline().strip() + + while len(line) == 0: # Skip whitespace, as a precaution + line = crdfile.readline().strip() + + intitle = True + while intitle: + self.title.append(line) + line = crdfile.readline().strip() + if len(line) == 0: + intitle = False + elif line[0] != '*': + intitle = False + else: + intitle = True + + while len(line) == 0: # Skip whitespace + line = crdfile.readline().strip() + + try: + self.natom = int(line.split()[0]) + for row in range(self.natom): + line = crdfile.readline().split() + self.atomno.append(int(line[0])) + self.resno.append(int(line[1])) + self.resname.append(line[2]) + self.atname.append(line[3]) + self.coords.append(float(line[4])) + self.coords.append(float(line[5])) + self.coords.append(float(line[6])) + self.segid.append(line[7]) + self.resid.append(line[8]) + self.weighting.append(float(line[9])) + + if len(self.coords) != 3 * self.natom: + raise RuntimeError(f"{len(self.coords)} coordinates are incompatible with {self.natom} atoms") + except (ValueError, IndexError): + raise CharmmError('Error parsing CHARMM coordinate file') + self.coords = np.array(self.coords).reshape((-1, self.natom, 3)) + + @staticmethod + def write(struct, dest): + """ Writes a CHARMM coordinate file from a structure + + Parameters + ---------- + struct : :class:`parmed.structure.Structure` + The input structure to write the CHARMM coordinate file from + dest : str or file-like object + The file name or file object to write the coordinate file to + """ + if isinstance(dest, str): + dest = io.genopen(dest, 'w') + own_handle = True + else: + own_handle = False + + dest.write('* GENERATED BY PARMED (HTTPS://GITHUB.COM/PARMED/PARMED)\n') + dest.write('*\n') + dest.write(f"{len(struct.atoms):10d} EXT\n") + add = 0 if struct.residues[0].number > 0 else 1-struct.residues[0].number + for i, atom in enumerate(struct.atoms): + res = atom.residue + segid = res.segid.strip() or res.chain.strip() or 'SYS' + dest.write( + f'{i + 1:10d}{atom.residue.number + add:10d} {atom.residue.name:<8s} {atom.name:<8s}' + f'{atom.xx:20.10f}{atom.xy:20.10f}{atom.xz:20.10f} {segid:<8s} ' + f'{str(atom.residue.number):<8s}{0:20.10f}\n' + ) + if own_handle: + dest.close() + +class CharmmRstFile(metaclass=FileFormatType): + """ + Reads and parses data, velocities and coordinates from a CHARMM restart + file (.rst) of file name 'fname' into class attributes + + Parameters + ---------- + fname : str + Name of the restart file to parse + + Attributes + ---------- + natom : int + Number of atoms in the system + resname : list of str + Names of all residues in the system + coordinates : np.ndarray shape(1, natom, 3) + List of all coordinates in the format [x1, y1, z1, x2, y2, z2, ...] + coordinatesold : np.ndarray shape(1, natom, 3) + List of all old coordinates in the format [x1, y1, z1, x2, y2, z2, ...] + velocities : np.ndarray shape(1, natom, 3) + List of all velocities in the format [x1, y1, z1, x2, y2, z2, ...] + positions : natom x 3 distance Quantity + 2-D list of all coordinates with the appropriate distance unit attached. + Has the format [ [x1, y1, z1], [x2, y2, z2], ... ] + positionsold : natom x 3 distance Quantity + 2-D list of all old coordinates with the appropriate distance unit + attached. Has the format [ [x1, y1, z1], [x2, y2, z2], ... ] + """ + + @staticmethod + def id_format(filename): + """ Identifies the file type as a CHARMM restart file + + Parameters + ---------- + filename : str + Name of the file to check format for + + Returns + ------- + is_fmt : bool + True if it is a CHARMM restart file + """ + with closing(io.genopen(filename)) as f: + line = f.readline() + return line.startswith('REST') + + def __init__(self, fname): + self.header = [] + self.title = [] + self.enrgstat = [] + self.coordsold = [] + self.coords = [] + self.vels = [] + + self.ff_version = 0 + self.natom = 0 + self.npriv = 0 + self.nstep = 0 + self.nsavc = 0 + self.nsavv = 0 + self.jhstrt = 0 + + self._parse(fname) + + @property + def coordinates(self): + return self.coords + + @property + def coordinatesold(self): + return self.coordsold + + @property + def positions(self): + """ Atomic positions with units """ + return [Vec3(*xyz) for xyz in self.coords[0]] * u.angstroms + + @property + def positionsold(self): + """ Old atomic positions with units """ + return [Vec3(*xyz) for xyz in self.coordsold[0]] * u.angstroms + + @property + def velocities(self): + """ Atomic velocities in Angstroms/picoseconds """ + return self.vels + + @property + def box(self): + return None + + def _parse(self, fname): + + with closing(io.genopen(fname, 'r')) as crdfile: + readingHeader = True + while readingHeader: + line = crdfile.readline() + if not len(line): + raise CharmmError('Premature end of file') + line = line.strip() + words = line.split() + if len(line) != 0: + if words[0] == 'ENERGIES' or words[0] == '!ENERGIES': + readingHeader = False + else: + self.header.append(line.strip()) + else: + self.header.append(line.strip()) + + for row in range(len(self.header)): + if len(self.header[row].strip()) != 0: + line = self.header[row].strip().split() + if line[0][0:5] == 'NATOM' or line[0][0:6] == '!NATOM': + try: + line = self.header[row+1].strip().split() + self.natom = int(line[0]) + self.npriv = int(line[1]) # num. previous steps + self.nstep = int(line[2]) # num. steps in file + self.nsavc = int(line[3]) # coord save frequency + self.nsavv = int(line[4]) # velocities " + self.jhstrt = int(line[5]) # Num total steps? + break + except (ValueError, IndexError): + raise CharmmError('Problem parsing CHARMM restart') + + self.scan(crdfile, '!XOLD') + self._get_formatted_crds(crdfile, self.coordsold) + self.coordsold = np.array(self.coordsold).reshape((-1,self.natom,3)) + + self.scan(crdfile, '!VX') + self._get_formatted_crds(crdfile, self.vels) + self.vels = np.array(self.vels).reshape((-1, self.natom, 3)) + # Convert velocities to angstroms/ps + self.vels *= ONE_TIMESCALE + + self.scan(crdfile, '!X') + self._get_formatted_crds(crdfile, self.coords) + self.coords = np.array(self.coords).reshape((-1, self.natom, 3)) + + def scan(self, handle, str, r=0): # read lines in file till 'str' is found + scanning = True + + if r: + handle.seek(0) + + while scanning: + line = handle.readline() + if not line: + raise CharmmError('Premature end of file') + + if len(line.strip()) != 0: + if line.strip().split()[0][0:len(str)] == str: + scanning = False + + def _get_formatted_crds(self, crdfile, crds): + for row in range(self.natom): + line = crdfile.readline() + + if not line: + raise CharmmError('Premature end of file') + + if len(line) < 3 * CHARMLEN: + raise CharmmError( + "Less than 3 coordinates present in coordinate row or coords may be truncated." + ) + + line = line.replace('D','E') # CHARMM uses 'D' for exponentials + + # CHARMM uses fixed format (len = CHARMLEN = 22) for crds in .rst's + crds.append(float(line[0:CHARMLEN])) + crds.append(float(line[CHARMLEN:2*CHARMLEN])) + crds.append(float(line[2*CHARMLEN:3*CHARMLEN])) diff --git a/pmx/workflow/parmed/charmm/parameters.py b/pmx/workflow/parmed/charmm/parameters.py new file mode 100644 index 00000000..8c4e5bd6 --- /dev/null +++ b/pmx/workflow/parmed/charmm/parameters.py @@ -0,0 +1,1233 @@ +""" +This module contains classes for parsing and processing CHARMM parameter, +topology, and stream files. +""" +import os +import re +import warnings +from copy import copy as _copy +from collections import OrderedDict +from itertools import combinations + +from ..constants import TINY +from ..exceptions import CharmmError, ParameterWarning +from ..modeller import PatchTemplate, ResidueTemplate +from ..parameters import ParameterSet +from ..periodic_table import AtomicNum, element_by_mass +from ..topologyobjects import (AngleType, Atom, AtomType, BondType, CmapType, + DihedralType, DihedralTypeList, ImproperType, + NoUreyBradley, DrudeAtom, DrudeAnisotropy) +from ..utils.io import genopen +from ._charmmfile import CharmmFile, CharmmStreamFile + +_penaltyre = re.compile(r'penalty\s*=\s*([\d\.]+)') + +class _EmptyStringIterator(object): + """ Always yields an empty string """ + def __iter__(self): + while True: + yield '' + def __getitem__(self, idx): + return '' + +def _typeconv(name): + if isinstance(name, int): + return name + if name.upper() == name: + return name.replace('*', 'STR').replace('+', 'P').replace('-', 'M')[:6] + # Lowercase letters present -- decorate the type name with LTU -- + # Lower To Upper + return f'{name.upper()}LTU'.replace('*', 'STR').replace('+', 'P').replace('-', 'M')[:6] + +class CharmmImproperMatchingMixin(object): + """ Implements CHARMM-style improper matching """ + + def match_improper_type(self, a1, a2, a3, a4): + """ Matches an improper type based on atom type names """ + typ = self._match_improper_with_typemap(self.improper_types, a1, a2, a3, a4) + if typ is None: + typ = self._match_improper_with_typemap(self.improper_periodic_types, a1, a2, a3, a4) + return typ + + def _match_improper_with_typemap(self, typemap, a1, a2, a3, a4): + + if (a1, a2, a3, a4) in typemap: return typemap[(a1, a2, a3, a4)] + if (a4, a3, a2, a1) in typemap: return typemap[(a4, a3, a2, a1)] + + # Now try any of the sortings. The documented CHARMM ordering does not seem to work for + # all systems CHARMM supports :( + + key = tuple(sorted([a1, a2, a3, a4])) + if self._improper_key_map.get(key, None) in typemap: + return typemap[self._improper_key_map[key]] + + for exact1, exact2, exact3 in combinations((a1, a2, a3, a4), 3): + key = tuple(sorted([exact1, exact2, exact3, 'X'])) + if self._improper_key_map.get(key, None) in typemap: + return typemap[self._improper_key_map[key]] + + for exact1, exact2 in combinations((a1, a2, a3, a4), 2): + key = tuple(sorted([exact1, exact2, 'X', 'X'])) + if self._improper_key_map.get(key, None) in typemap: + return typemap[self._improper_key_map[key]] + + return None + +class CharmmParameterSet(ParameterSet, CharmmImproperMatchingMixin): + """ + Stores a parameter set defined by CHARMM files. It stores the equivalent of + the information found in the MASS section of the CHARMM topology file + (TOP/RTF) and all of the information in the parameter files (PAR) + + Parameters + ---------- + *filenames : variable length arguments of str + The list of topology, parameter, and stream files to load into the + parameter set. The following file type suffixes are recognized: + .rtf, .top -- Residue topology file + .par, .prm -- Parameter file + .str -- Stream file + .inp -- If "par" is in the file name, it is a parameter file. If + "top" is in the file name, it is a topology file. + Otherwise, ValueError is raised. + + See Also + -------- + :class:`parmed.parameters.ParameterSet` + """ + + def __copy__(self): + other = super(CharmmParameterSet, self).__copy__() + other._declared_nbrules = self._declared_nbrules + return other + + @staticmethod + def _convert(data, type, msg='', line_index=None, line=None): + """ + Converts a data type to a desired type, raising CharmmError if it + fails + """ + try: + return type(data) + except ValueError: + msg = f'Could not convert {msg} to {type}\n' + if line_index is not None: + msg += f'input line {line_index}\n' + if line is not None: + msg += f'input line: {line}\n' + raise CharmmError(msg) + + def __init__(self, *args): + # Instantiate the list types + super(CharmmParameterSet, self).__init__() + self.parametersets = [] + self._declared_nbrules = False + + # Load all of the files + tops, pars, strs = [], [], [] + for arg in args: + if arg.endswith('.rtf') or arg.endswith('.top'): + tops.append(arg) + elif arg.endswith('.par') or arg.endswith('.prm'): + pars.append(arg) + elif arg.endswith('.str'): + strs.append(arg) + elif arg.endswith('.inp'): + # Only consider the file name (since the directory is likely + # "toppar" and will screw up file type detection) + fname = os.path.split(arg)[1] + if 'par' in fname: + pars.append(arg) + elif 'top' in fname: + tops.append(arg) + else: + raise ValueError(f'Unrecognized file type: {arg}') + else: + raise ValueError(f'Unrecognized file type: {arg}') + for top in tops: + self.read_topology_file(top) + for par in pars: + self.read_parameter_file(par) + for strf in strs: + self.read_stream_file(strf) + + @classmethod + def from_parameterset(cls, params, copy=False): + """ + Instantiates a CharmmParameterSet from another ParameterSet (or + subclass). The main thing this feature is responsible for is converting + lower-case atom type names into all upper-case and decorating the name + to ensure each atom type name is unique. + + Parameters + ---------- + params : :class:`parmed.parameters.ParameterSet` + ParameterSet containing the list of parameters to be converted to a + CHARMM-compatible set + copy : bool, optional + If True, the returned parameter set is a deep copy of ``params``. If + False, the returned parameter set is a shallow copy, and the + original set may be modified if any lower-case atom type names are + present. Default is False. + + Returns + ------- + new_params : CharmmParameterSet + The parameter set whose atom type names are converted to all + upper-case + """ + new_params = cls() + if copy: + do_copy = lambda x: _copy(x) + else: + do_copy = lambda x: x + # Convert all parameters + id_typemap = dict() + def copy_paramtype(key, typ, dict): + if isinstance(key, str): + key = _typeconv(key) + elif isinstance(key, tuple): + key = tuple(_typeconv(k) for k in key) + # NoUreyBradley should never be copied + if typ is NoUreyBradley: + dict[key] = NoUreyBradley + elif id(typ) in id_typemap: + dict[key] = id_typemap[id(typ)] + else: + newtype = do_copy(typ) + id_typemap[id(typ)] = newtype + dict[key] = newtype + + for key, atom_type in params.atom_types_tuple.items(): + atom_type.name = _typeconv(atom_type.name) + copy_paramtype(key, atom_type, new_params.atom_types_tuple) + for typename, atom_type in params.atom_types.items(): + atom_type.name = _typeconv(atom_type.name) + copy_paramtype(typename, atom_type, new_params.atom_types) + for idx, atom_type in params.atom_types_int.items(): + atom_type.name = _typeconv(atom_type.name) + copy_paramtype(idx, atom_type, new_params.atom_types_int) + + for key, typ in params.bond_types.items(): + copy_paramtype(key, typ, new_params.bond_types) + for key, typ in params.angle_types.items(): + copy_paramtype(key, typ, new_params.angle_types) + for key, typ in params.urey_bradley_types.items(): + copy_paramtype(key, typ, new_params.urey_bradley_types) + for key, typ in params.dihedral_types.items(): + copy_paramtype(key, typ, new_params.dihedral_types) + for key, typ in params.improper_periodic_types.items(): + copy_paramtype(key, typ, new_params.improper_periodic_types) + for key, typ in params.improper_types.items(): + copy_paramtype(key, typ, new_params.improper_types) + for key, typ in params.cmap_types.items(): + assert len(key) == 8, f'{len(key)}-key cmap type detected!' + copy_paramtype(key, typ, new_params.cmap_types) + for key, typ in params.nbfix_types.items(): + copy_paramtype(key, typ, new_params.nbfix_types) + + return new_params + + @classmethod + def from_structure(cls, struct): + """ Extracts known parameters from a Structure instance + + Parameters + ---------- + struct : :class:`parmed.structure.Structure` + The parametrized ``Structure`` instance from which to extract + parameters into a ParameterSet + + Returns + ------- + params : :class:`ParameterSet` + The parameter set with all parameters defined in the Structure + + Notes + ----- + The parameters here are copies of the ones in the Structure, so + modifying the generated ParameterSet will have no effect on ``struct``. + Furthermore, the *first* occurrence of each parameter will be used. If + future ones differ, they will be silently ignored, since this is + expected behavior in some instances (like with Gromacs topologies in the + ff99sb-ildn force field). + + Dihedrals are a little trickier. They can be multi-term, which can be + represented either as a *single* entry in dihedrals with a type of + DihedralTypeList or multiple entries in dihedrals with a DihedralType + parameter type. In this case, the parameter is constructed from either + the first DihedralTypeList found or the first DihedralType of each + periodicity found if no matching DihedralTypeList is found. + """ + return cls.from_parameterset( + ParameterSet.from_structure(struct, allow_unequal_duplicates=False) + ) + + @classmethod + def load_set(cls, tfile=None, pfile=None, sfiles=None): + """ + Instantiates a CharmmParameterSet from a Topology file and a Parameter + file (or just a Parameter file if it has all information) + + Parameters + ---------- + tfile : str + The name of the Topology (RTF/TOP) file to parse + pfile : str + The name of the Parameter (PAR/PRM) file to parse + sfiles : list(str) + Iterable of stream (STR) file names + + Returns + ------- + New CharmmParameterSet populated with parameters found in the provided + files + + Notes + ----- + The RTF file is read first (if provided), followed by the PAR file, + followed by the list of stream files (in the order they are provided). + Parameters in each stream file will overwrite those that came before (or + simply append to the existing set if they are different) + """ + inst = cls() + if tfile is not None: + inst.read_topology_file(tfile) + if pfile is not None: + inst.read_parameter_file(pfile) + if isinstance(sfiles, str): + # The API docstring requests a list, but allow for users to pass a + # string with a single filename instead + inst.read_stream_file(sfiles) + elif sfiles is not None: + for sfile in sfiles: + inst.read_stream_file(sfile) + return inst + + def read_parameter_file(self, pfile, comments=None): + """ + Reads all of the parameters from a parameter file. Versions 36 and + later of the CHARMM force field files have an ATOMS section defining + all of the atom types. Older versions need to load this information + from the RTF/TOP files. + + Parameters + ---------- + pfile : str or list of lines + Name of the CHARMM parameter file to read or list of lines to parse + as a file + comments : list of str, optional + List of comments on each of the pfile lines (if pfile is a list of + lines) + + Notes + ----- + The atom types must all be loaded by the end of this routine. Either + supply a PAR file with atom definitions in them or read in a RTF/TOP + file first. Failure to do so will result in a raised RuntimeError. + """ + conv = CharmmParameterSet._convert + if isinstance(pfile, str): + own_handle = True + f = CharmmFile(pfile) + else: + own_handle = False + f = pfile + if not isinstance(f, CharmmFile) and comments is None: + comments = _EmptyStringIterator() + # What section are we parsing? + section = None + # The current cmap we are building (these span multiple lines) + current_cmap = None + current_cmap2 = None + current_cmap_data = [] + current_cmap_res = 0 + nonbonded_types = dict() # Holder + parameterset = None + declared_geometric = False + for i, line in enumerate(f): + line = line.strip() + try: + comment = f.comment + except AttributeError: + comment = comments[i] + if not line: + # This is a blank line + continue + if parameterset is None and line.strip().startswith('*>>'): + parameterset = line.strip()[1:78] + continue + # Set section if this is a section header + if line.upper().startswith('ATOM'): + section = 'ATOMS' + continue + if line.upper().startswith('BOND'): + section = 'BONDS' + continue + if line.upper().startswith('ANGLE') or line.upper().startswith('THETA'): + section = 'ANGLES' + continue + if line.upper().startswith('DIHE') or line.upper().startswith('PHI'): + section = 'DIHEDRALS' + continue + if line.upper().startswith('IMPROPER') or line.upper().startswith('IMPHI'): + section = 'IMPROPER' + continue + if line.upper().startswith('CMAP'): + section = 'CMAP' + continue + if line.upper().startswith('NONBONDED'): + read_first_nonbonded = declared_geometric = False + section = 'NONBONDED' + # Get nonbonded keywords + words = line.split()[1:] + scee = None + for i, word in enumerate(words): + if word.upper() == 'E14FAC': + try: + scee = 1 / float(words[i+1]) + except (ValueError, IndexError): + raise CharmmError('Could not parse 1-4 electrostatic scaling factor ' + 'from NONBONDED card') + if self._declared_nbrules: + if len(self.dihedral_types) > 0: + # We already specified it -- make sure it's the same + # as the one we specified before + _, dt0 = next(iter(self.dihedral_types.items())) + diff = abs(dt0[0].scee - scee) + if diff > TINY: + raise CharmmError('Inconsistent 1-4 scalings') + else: + for key, dtl in self.dihedral_types.items(): + for dt in dtl: + dt.scee = scee + elif word.upper().startswith('GEOM'): + if self._declared_nbrules and self.combining_rule != 'geometric': + raise CharmmError('Cannot combine parameter files with different ' + 'combining rules') + self.combining_rule = 'geometric' + declared_geometric = True + continue + if line.upper().startswith('NBFIX'): + section = 'NBFIX' + continue + if line.upper().startswith('HBOND'): + section = None + continue + if line.upper().startswith('THOLE'): + section = None + continue + # It seems like files? sections? can be terminated with 'END' + if line[:3].upper() == 'END': + section = None + continue + # If we have no section, skip + if section is None: continue + # See if our comments define a penalty for this line + pens = _penaltyre.findall(comment) + if len(pens) == 1: + penalty = float(pens[0]) + else: + penalty = None + # Now handle each section specifically + if section.upper() == 'ATOMS': + if not line.upper().startswith('MASS'): continue # Should this happen? + words = line.split() + if words[0].upper() == 'END': + continue + try: + idx = conv(words[1], int, 'atom type', line_index=i, line=line) + name = words[2].upper() + mass = conv(words[3], float, 'atom mass', line_index=i, line=line) + except IndexError: + raise CharmmError('Could not parse MASS section.') + # The parameter file might or might not have an element name + try: + elem = words[4].upper() + if len(elem) == 2: + elem = elem[0] + elem[1].lower() + atomic_number = AtomicNum[elem] + except (IndexError, KeyError): + # Figure it out from the mass + atomic_number = AtomicNum[element_by_mass(mass)] + atype = AtomType(name=name, number=idx, mass=mass, atomic_number=atomic_number) + self.atom_types_str[atype.name] = atype + self.atom_types_int[atype.number] = atype + self.atom_types_tuple[(atype.name, atype.number)] = atype + continue + if section.upper() == 'BONDS': + words = line.split() + if words[0].upper() == 'END': + continue + try: + type1 = words[0].upper() + type2 = words[1].upper() + k = conv(words[2], float, 'bond force constant', line_index=i, line=line) + req = conv(words[3], float, 'bond equilibrium dist', line_index=i, line=line) + except IndexError: + raise CharmmError('Could not parse bonds.') + key = (min(type1, type2), max(type1, type2)) + bond_type = BondType(k, req) + if key in self.bond_types: + # See if existing bond type has a different value and replaces it with a warning + if self.bond_types[key] != bond_type: + # Replace. Warn if they are different + warnings.warn('Replacing bond %r, %r with %r' % + (key, self.bond_types[key], bond_type), ParameterWarning) + self.bond_types[(type1, type2)] = bond_type + self.bond_types[(type2, type1)] = bond_type + else: # key not present + self.bond_types[(type1, type2)] = bond_type + self.bond_types[(type2, type1)] = bond_type + bond_type.penalty = penalty + continue + if section.upper() == 'ANGLES': + words = line.split() + if words[0].upper() == 'END': + continue + try: + type1 = words[0].upper() + type2 = words[1].upper() + type3 = words[2].upper() + k = conv(words[3], float, 'angle force constant', line_index=i, line=line) + theteq = conv(words[4], float, 'angle equilibrium value', line_index=i, line=line) + except IndexError: + raise CharmmError('Could not parse angles.') + + angle_type = AngleType(k, theteq) + key = (type1, type2, type3) + if key in self.angle_types: + # See if the existing angle type list has a different value + # and replaces it with a warning + if self.angle_types[key] != angle_type: + # Replace. Warn if they are different + warnings.warn('Replacing angle %r, %r with %r' % + (key, self.angle_types[key], angle_type), ParameterWarning) + self.angle_types[(type1, type2, type3)] = angle_type + self.angle_types[(type3, type2, type1)] = angle_type + else: # key not present + self.angle_types[(type1, type2, type3)] = angle_type + self.angle_types[(type3, type2, type1)] = angle_type + # See if we have a urey-bradley + try: + ubk = conv(words[5], float, 'Urey-Bradley force constant', line_index=i, line=line) + ubeq = conv(words[6], float, 'Urey-Bradley equil. value', line_index=i, line=line) + ubtype = BondType(ubk, ubeq) + ubtype.penalty = penalty + except IndexError: + ubtype = NoUreyBradley + self.urey_bradley_types[(type1, type2, type3)] = ubtype + self.urey_bradley_types[(type3, type2, type1)] = ubtype + angle_type.penalty = penalty + continue + if section.upper() == 'DIHEDRALS': + words = line.split() + if words[0].upper == 'END': + continue + try: + type1 = words[0].upper() + type2 = words[1].upper() + type3 = words[2].upper() + type4 = words[3].upper() + k = conv(words[4], float, 'dihedral force constant', line_index=i, line=line) + n = conv(words[5], float, 'dihedral periodicity', line_index=i, line=line) + phase = conv(words[6], float, 'dihedral phase', line_index=i, line=line) + except IndexError: + raise CharmmError('Could not parse dihedrals.') + key = (type1, type2, type3, type4) + # See if this is a second (or more) term of the dihedral group + # that's already present. + dihedral = DihedralType(k, n, phase) + dihedral.penalty = penalty + if key in self.dihedral_types: + # See if the existing dihedral type list has a term with + # the same periodicity -- If so, replace it + replaced = False + for i, dtype in enumerate(self.dihedral_types[key]): + if dtype.per == dihedral.per: + # Replace. Warn if they are different + if dtype != dihedral: + warnings.warn( + f'Replacing dihedral {dtype} with {dihedral}', ParameterWarning + ) + self.dihedral_types[key][i] = dihedral + replaced = True + break + if not replaced: + self.dihedral_types[key].append(dihedral) + else: # key not present + dtl = DihedralTypeList() + dtl.append(dihedral) + self.dihedral_types[(type1, type2, type3, type4)] = dtl + self.dihedral_types[(type4, type3, type2, type1)] = dtl + continue + if section.upper() == 'IMPROPER': + words = line.split() + if words[0].upper() == 'END': + continue + try: + type1 = words[0].upper() + type2 = words[1].upper() + type3 = words[2].upper() + type4 = words[3].upper() + k = conv(words[4], float, 'improper force constant', line_index=i, line=line) + theteq = conv(words[5], float, 'improper equil. value', line_index=i, line=line) + except IndexError: + raise CharmmError('Could not parse dihedrals.') + # If we have a 7th column, that is the real psi0 (and the 6th + # is the multiplicity, which will indicate this is a periodic + # improper torsion (so it needs to be added to the + # improper_periodic_types list) + try: + tmp = conv(words[6], float, 'improper equil. value', line_index=i, line=line) + except IndexError: + per = 0 + else: + per = int(theteq) + theteq = tmp + # Improper types seem not to always have the central atom + # defined in the first place, so just have the key a fully + # sorted list. We still depend on the PSF having properly + # ordered improper atoms + key = (type1, type2, type3, type4) + self._improper_key_map[tuple(sorted(key))] = key + if per == 0: + improp = ImproperType(k, theteq) + self.improper_types[key] = improp + else: + improp = DihedralType(k, per, theteq) + self.improper_periodic_types[key] = improp + improp.improper = True + improp.penalty = penalty + continue + if section.upper() == 'CMAP': + # This is the most complicated part, since cmap parameters span + # many lines. We won't do much error catching here. + words = line.split() + if words[0].upper() == 'END': + continue + try: + holder = [float(w) for w in words] + current_cmap_data.extend(holder) + except ValueError: + # We assume this is a definition of a new CMAP, so + # terminate the last CMAP if applicable + if current_cmap is not None: + # We have a map to terminate + ty = CmapType(current_cmap_res, current_cmap_data) + self.cmap_types[current_cmap] = ty + self.cmap_types[current_cmap2] = ty + try: + type1 = words[0].upper() + type2 = words[1].upper() + type3 = words[2].upper() + type4 = words[3].upper() + type5 = words[4].upper() + type6 = words[5].upper() + type7 = words[6].upper() + type8 = words[7].upper() + res = conv(words[8], int, 'CMAP resolution', line_index=i, line=line) + except IndexError: + raise CharmmError('Could not parse CMAP data.') + # order the torsions independently + k1 = [type1, type2, type3, type4, type5, type6, type7, type8] + k2 = [type8, type7, type6, type5, type4, type3, type2, type1] + current_cmap = tuple(min(k1, k2)) + current_cmap2 = tuple(max(k1, k2)) + current_cmap_res = res + current_cmap_data = [] + continue + if section.upper() == 'NONBONDED': + # Now get the nonbonded values + words = line.split() + if words[0].upper == 'END': + continue + try: + atype = words[0].upper() + # 1st column is ignored + epsilon = conv(words[2], float, 'vdW epsilon term', line_index=i, line=line) + rmin = conv(words[3], float, 'vdW Rmin/2 term', line_index=i, line=line) + except (IndexError, CharmmError): + # If we haven't read our first nonbonded term yet, we may + # just be parsing the settings that should be used. So + # soldier on + if read_first_nonbonded: raise + for i, word in enumerate(words): + if word.upper() == 'E14FAC': + try: + scee = 1 / float(words[i+1]) + except (ValueError, IndexError): + raise CharmmError('Could not parse electrostatic scaling constant') + if self._declared_nbrules: + # We already specified it -- make sure it's the + # same as the one we specified before + _, dt0 = next(iter(self.dihedral_types.items())) + diff = abs(dt0[0].scee - scee) + if diff > TINY: + raise CharmmError('Inconsistent 1-4 scalings') + else: + for key, dtl in self.dihedral_types.items(): + for dt in dtl: + dt.scee = scee + elif word.upper().startswith('GEOM'): + if self._declared_nbrules and self.combining_rule != 'geometric': + raise CharmmError( + 'Cannot combine parameter files with different combining rules' + ) + self.combining_rule = 'geometric' + declared_geometric = True + continue + else: + # OK, we've read our first nonbonded section for sure now. + # Make sure we did not try to read in a str file that did + # not define GEOM if a previous file did, since + # Lorentz-Berthelot and geometric combining rules are + # incompatible + if (self._declared_nbrules and self.combining_rule == 'geometric' and + not declared_geometric): + raise CharmmError('Cannot combine parameter files with ' + 'different combining rules') + read_first_nonbonded = True + self._declared_nbrules = True + # See if we have 1-4 parameters + try: + # 4th column is ignored + eps14 = conv(words[5], float, '1-4 vdW epsilon term', line_index=i, line=line) + rmin14 = conv(words[6], float, '1-4 vdW Rmin/2 term', line_index=i, line=line) + except IndexError: + eps14 = rmin14 = None + nonbonded_types[atype] = [epsilon, rmin, eps14, rmin14] + continue + if section.upper() == 'NBFIX': + words = line.split() + if words[0].upper() == 'END': + continue + try: + at1 = words[0].upper() + at2 = words[1].upper() + emin = abs(conv(words[2], float, 'NBFIX Emin', line_index=i, line=line)) + rmin = conv(words[3], float, 'NBFIX Rmin', line_index=i, line=line) + try: + emin14 = abs(conv(words[4], float, 'NBFIX Emin 1-4', line_index=i, line=line)) + rmin14 = conv(words[5], float, 'NBFIX Rmin 1-4', line_index=i, line=line) + except IndexError: + emin14 = rmin14 = None + try: + self.atom_types_str[at1].add_nbfix(at2, rmin, emin, rmin14, emin14) + self.atom_types_str[at2].add_nbfix(at1, rmin, emin, rmin14, emin14) + except KeyError: + # Some stream files define NBFIX terms with an atom that + # is defined in another toppar file that does not + # necessarily have to be loaded. As a result, not every + # NBFIX found here will necessarily need to be applied. + # If we can't find a particular atom type, don't bother + # adding that nbfix and press on + pass + except IndexError: + raise CharmmError('Could not parse NBFIX terms.') + self.nbfix_types[(min(at1, at2), max(at1, at2))] = (emin, rmin) + # If we had any CMAP terms, then the last one will not have been added + # yet. Add it here + if current_cmap is not None: + typ = CmapType(current_cmap_res, current_cmap_data) + self.cmap_types[current_cmap] = typ + self.cmap_types[current_cmap2] = typ + # Now we're done. Load the nonbonded types into the relevant AtomType + # instances. In order for this to work, all keys in nonbonded_types + # must be in the self.atom_types_str dict. Raise a RuntimeError if this + # is not satisfied + try: + for key in nonbonded_types: + self.atom_types_str[key].set_lj_params(*nonbonded_types[key]) + except KeyError: + warnings.warn('Atom type %s not present in AtomType list' % key, ParameterWarning) + if parameterset is not None: + self.parametersets.append(parameterset) + if own_handle: + f.close() + + def read_topology_file(self, tfile): + """ + Reads _only_ the atom type definitions from a topology file. This is + unnecessary for versions 36 and later of the CHARMM force field. + + Parameters + ---------- + tfile : str + Name of the CHARMM topology file to read + """ + conv = CharmmParameterSet._convert + if isinstance(tfile, str): + own_handle = True + f = iter(CharmmFile(tfile)) + else: + own_handle = False + f = tfile + hpatch = tpatch = None # default Head and Tail patches + residues = OrderedDict() + patches = OrderedDict() + hpatches = OrderedDict() + tpatches = OrderedDict() + line = next(f) + line_index = 0 + skip_adding_residue = False + try: + while line: + line = line.strip() + if line[:4].upper() == 'MASS': + words = line.split() + try: + idx = conv(words[1], int, 'atom type', line_index=line_index, line=line) + name = words[2].upper() + mass = conv(words[3], float, 'atom mass', line_index=line_index, line=line) + except IndexError: + raise CharmmError('Could not parse MASS section of %s' % tfile) + # The parameter file might or might not have an element name + try: + elem = words[4].upper() + if len(elem) == 2: + elem = elem[0] + elem[1].lower() + atomic_number = AtomicNum[elem] + except (IndexError, KeyError): + # Figure it out from the mass + atomic_number = AtomicNum[element_by_mass(mass)] + atype = AtomType(name=name, number=idx, mass=mass, atomic_number=atomic_number) + self.atom_types_str[atype.name] = atype + self.atom_types_int[atype.number] = atype + self.atom_types_tuple[(atype.name, atype.number)] = atype + elif line[:4].upper() == 'DECL': + pass # Not really sure what this means + elif line[:4].upper() == 'DEFA': + words = line.split() + if len(words) < 5: + warnings.warn('DEFA line has %d tokens; expected 5' % len(words)) + else: + it = iter(words[1:5]) + for tok, val in zip(it, it): + if val.upper() == 'NONE': + val = None + if tok.upper().startswith('FIRS'): + hpatch = val + elif tok.upper() == 'LAST': + tpatch = val + else: + warnings.warn(f'DEFA patch {val} unknown') + elif line[:4].upper() in ('RESI', 'PRES'): + restype = line[:4].upper() + # Get the residue definition + words = line.split() + resname = words[1].upper() + if resname in self.residues: + warnings.warn(f'Replacing residue {resname}', ParameterWarning) + # Assign default patches + hpatches[resname] = hpatch + tpatches[resname] = tpatch + try: + charge = float(words[2]) + except (IndexError, ValueError): + warnings.warn(f'No charge for {resname}') + if restype == 'RESI': + res = ResidueTemplate(resname) + elif restype == 'PRES': + res = PatchTemplate(resname) + else: + assert False, 'restype != RESI or PRES' + skip_adding_residue = False + line = next(f) + group = [] + ictable = [] + while line: + line = line.lstrip() + if line[:5].upper() == 'GROUP': + if group: + res.groups.append(group) + group = [] + elif line[:4].upper() == 'ATOM': + words = line.split() + name = words[1].upper() + type = words[2].upper() + charge = float(words[3]) + if 'ALPHA' in words: + # This is a polarizable atom. + alpha = float(words[words.index('ALPHA')+1]) + thole = 1.3 + drude_type = 'DRUD' + if 'THOLE' in words: + thole = float(words[words.index('THOLE')+1]) + if 'TYPE' in words: + drude_type = words[words.index('TYPE')+1] + atom = DrudeAtom( + name=name, + type=type, + charge=charge, + alpha=alpha, + thole=thole, + drude_type=drude_type + ) + else: + atom = Atom(name=name, type=type, charge=charge) + group.append(atom) + res.add_atom(atom) + elif line[:6].upper() == 'DELETE': + words = line.split() + name = words[2].upper() + entity_type = words[1].upper() + if entity_type == 'ATOM': + res.delete_atoms.append(name) + elif entity_type == 'IMPR': + res.delete_impropers.append(words[2:5]) + else: + warnings.warn( + f'WARNING: Ignoring "{line.strip()}" because entity type ' + f'{entity_type} not used.' + ) + elif line.strip().upper() and line.split()[0].upper() in ('BOND', 'DOUBLE'): + it = iter([w.upper() for w in line.split()[1:]]) + for a1, a2 in zip(it, it): + if restype == 'PRES': + # Patches can have bonds that refer to atoms not in the patch, so store these in a list of tuples + order = 1 + if line.split()[0].upper() == 'DOUBLE': + order = 2 + res.add_bonds.append( (a1, a2, order) ) + continue + + if a1.startswith('-'): + res.head = res[a2] + continue + if a2.startswith('-'): + res.head = res[a1] + continue + if a1.startswith('+'): + res.tail = res[a2] + continue + if a2.startswith('+'): + res.tail = res[a1] + continue + res.add_bond(a1, a2) + elif line[:4].upper() == 'CMAP': + pass + elif line[:5].upper() == 'DONOR': + pass + elif line[:6].upper() == 'ACCEPT': + pass + elif line[:8].upper() == 'LONEPAIR': + # See: https://www.charmm.org/charmm/documentation/by-version/c40b1/params/doc/lonepair/ + # TODO: This currently doesn't handle some formats, like Note 3 in the above URL + words = line.split() + lptype_keyword = words[1][0:4].upper() + if not skip_adding_residue and lptype_keyword not in ['BISE', 'RELA']: + warnings.warn( + f'LONEPAIR type {words[1]} not supported; only BISEctor and ' + 'RELAtive supported' + ) + skip_adding_residue = True + break + a1, a2, a3, a4 = words[2:6] + keywords = {words[index][0:4].upper() : float(words[index+1]) + for index in range(6, len(words), 2) } + r = keywords['DIST'] # angstrom + theta = keywords['ANGL'] # degrees + phi = keywords['DIHE'] # degrees + lptypes = { 'BISE' : 'bisector', 'RELA' : 'relative' } + lonepair = (lptypes[lptype_keyword], a1, a2, a3, a4, r, theta, phi) # TODO: Define a LonePair object? + res.lonepairs.append(lonepair) + elif line[:2].upper() == 'IC': + words = line.split()[1:] + ictable.append( + ([w.upper() for w in words[:4]], [float(w) for w in words[4:]]) + ) + elif line[:3].upper() == 'END': + break + elif line[:5].upper() == 'PATCH': + it = iter(line.split()[1:]) + for tok, val in zip(it, it): + if val.upper() == 'NONE': val = None + if tok.upper().startswith('FIRS'): + hpatches[resname] = val + elif tok.upper().startswith('LAST'): + tpatches[resname] = val + elif line[:4].upper() in ('IMPR', 'IMPH'): + it = iter(w.upper() for w in line.split()[1:]) + for a1, a2, a3, a4 in zip(it, it, it, it): + res._impr.append((a1, a2, a3, a4)) + if a2[0] == '-' or a3[0] == '-' or a4 == '-': + res.head = res[a1] + elif line[:10].upper() == 'ANISOTROPY': + words = line.split() + atoms = [res[name] for name in words[1:5]] + keywords = {words[index].upper() : float(words[index+1]) + for index in range(5, len(words), 2)} + a11 = float(keywords['A11']) + a22 = float(keywords['A22']) + atoms[0].anisotropy = DrudeAnisotropy(*atoms, a11=a11, a22=a22) + elif line[:4].upper() in ('RESI', 'PRES', 'MASS'): + # Back up a line and bail + break + line = next(f) + if group: res.groups.append(group) + _fit_IC_table(res, ictable) + if skip_adding_residue: + # Do not add this residue to the lookup library + continue + elif restype == 'RESI': + residues[resname] = res + elif restype == 'PRES': + patches[resname] = res + else: + assert False, 'restype != RESI or PRES' + # We parsed a line we need to look at. So don't update the + # iterator + continue + # Get the next line and cycle through + line = next(f) + line_index += 1 + except StopIteration: + pass + + # Go through the patches and add the appropriate one + self.patches.update(patches) + for resname, res in residues.items(): + patch_name = hpatches[resname] + if patch_name is not None: + try: + res.first_patch = self.patches[patch_name] + except KeyError: + warnings.warn(f'Patch {patch_name} not found') + + patch_name = tpatches[resname] + if patch_name is not None: + try: + res.last_patch = self.patches[patch_name] + except KeyError: + warnings.warn(f'Patch {patch_name} not found') + # Now update the residues and patches with the ones we parsed here + self.residues.update(residues) + + if own_handle: f.close() + + def read_stream_file(self, sfile): + """ + Reads RTF and PAR sections from a stream file and dispatches the + sections to read_topology_file or read_parameter_file + + Parameters + ---------- + sfile : str or CharmmStreamFile + Stream file to parse + """ + if isinstance(sfile, CharmmStreamFile): + f = sfile + else: + f = CharmmStreamFile(sfile) + + title, section, comments = f.next_section() + while title is not None and section is not None: + words = title.lower().split() + if words[1] == 'rtf': + # This is a Residue Topology File section. + self.read_topology_file(iter(section)) + elif words[1].startswith('para'): + # This is a Parameter file section + self.read_parameter_file(section, comments) + title, section, comments = f.next_section() + + def write(self, top=None, par=None, stream=None, **kwargs): + """ Write a CHARMM parameter set to a file + + Parameters + ---------- + top : str or file-like object, optional + If provided, the atom types will be written to this file in RTF + format. + par : str or file-like object, optional + If provided, the parameters will be written to this file in PAR + format. Either this or the ``str`` argument *must* be provided + str : str or file-like object, optional + If provided, the atom types and parameters will be written to this + file as separate RTF and PAR cards that can be read as a CHARMM + stream file. Either this or the ``par`` argument *must* be provided + + Raises + ------ + ValueError if both par and str are None + """ + if stream is None and kwargs.get("str") is not None: + warnings.warn( + "Use 'stream' instead of 'str' to write CHARMM parameters", DeprecationWarning + ) + stream = kwargs["str"] + if stream is not None and kwargs.get("str") != stream: + warnings.warn( + f"Ignoring 'str' ({kwargs.get('str')}), using 'stream' ({stream}) instead", + DeprecationWarning, + ) + + if par is None and stream is None: + raise ValueError('Must specify either par *or* str') + + if top is not None: + if isinstance(top, str): + f = genopen(top, 'w') + ownhandle = True + else: + f = top + ownhandle = False + f.write('*>>>> CHARMM Topology file generated by ParmEd <<<<\n') + f.write('*\n') + self._write_top_to(f, True) + if ownhandle: f.close() + if par is not None: + if isinstance(par, str): + f = genopen(par, 'w') + ownhandle = True + else: + f = par + ownhandle = False + f.write('*>>>> CHARMM Parameter file generated by ParmEd <<<<\n') + f.write('*\n\n') + self._write_par_to(f) + if ownhandle: f.close() + if stream is not None: + if isinstance(stream, str): + f = genopen(stream, 'w') + ownhandle = True + else: + f = stream + ownhandle = False + self._write_str_to(f) + if ownhandle: + f.close() + + def _write_str_to(self, f): + """ Private method to write stream items to open file object """ + f.write('read rtf card\n* Topology generated by ParmEd\n*\n') + self._write_top_to(f, True) + f.write('\nread para card\n* Parameters generated by ParmEd\n*\n') + self._write_par_to(f) + + def _write_top_to(self, f, write_version): + """ Private method to write topology items to open file object """ + if write_version: + # This version is known to work + f.write('36 1\n') + f.write('\n') + for i, (_, atom) in enumerate(self.atom_types.items()): + f.write(f'MASS {i + 1:5d} {atom.name:<6s} {atom.mass:9.5f}\n') + if write_version: + f.write('\nEND\n') + + def _write_par_to(self, f): + """ Private method to write parameter items to open file object """ + # Find out what the 1-4 electrostatic scaling factors and the 1-4 + # van der Waals scaling factors are + scee, scnb = set(), set() + for _, typ in self.dihedral_types.items(): + for t in typ: + if t.scee: + scee.add(t.scee) + if t.scnb: + scnb.add(t.scnb) + if len(scee) > 1 or len(scnb) > 1: + raise ValueError('Mixed 1-4 scaling not supported') + scee = 1.0 if not scee else scee.pop() + scnb = 1.0 if not scnb else scnb.pop() + + f.write('ATOMS\n') + self._write_top_to(f, False) + f.write('\nBONDS\n') + written = set() + for key, typ in self.bond_types.items(): + if key in written: + continue + written.add(key) + written.add(tuple(reversed(key))) + f.write(f'{key[0]:<6s} {key[1]:<6s} {typ.k:7.2f} {typ.req:10.4f}\n') + f.write('\nANGLES\n') + written = set() + for key, typ in self.angle_types.items(): + if key in written: continue + written.add(key) + written.add(tuple(reversed(key))) + f.write(f'{key[0]:<6s} {key[1]:<6s} {key[2]:<6s} {typ.k:7.2f} {typ.theteq:8.2f}\n') + f.write('\nDIHEDRALS\n') + written = set() + for key, typ in self.dihedral_types.items(): + if key in written: continue + written.add(key) + written.add(tuple(reversed(key))) + for tor in typ: + f.write( + f'{key[0]:<6s} {key[1]:<6s} {key[2]:<6s} {key[3]:<6s} {tor.phi_k:11.4f} {tor.per:2d} {tor.phase:8.2f}\n' + ) + f.write('\nIMPROPERS\n') + written = set() + for key, typ in sorted(self.improper_periodic_types.items(), key=lambda x: x[0]): + f.write( + f'{key[0]:<6s} {key[1]:<6s} {key[2]:<6s} {key[3]:<6s} {typ.phi_k:11.4f} {int(typ.per):2d} {typ.phase:8.2f}\n' + ) + for key, typ in self.improper_types.items(): + f.write( + f'{key[0]:<6s} {key[1]:<6s} {key[2]:<6s} {key[3]:<6s} {typ.psi_k:11.4f} {0:2d} {typ.psi_eq:8.2f}\n' + ) + if self.cmap_types: + f.write('\nCMAPS\n') + written = set() + for key, typ in self.cmap_types.items(): + if key in written: continue + written.add(key); written.add(tuple(reversed(key))) + f.write( + f"{key[0]:<6s} {key[1]:<6s} {key[2]:<6s} {key[3]:<6s} {key[4]:<6s} " + f"{key[5]:<6s} {key[6]:<6s} {key[7]:<6s} {typ.resolution:5d}\n\n" + ) + i = 0 + for val in typ.grid: + if i: + if i % 5 == 0: + f.write('\n') + if i % typ.resolution == 0: + f.write('\n') + i = 0 + elif i % typ.resolution == 0: + f.write('\n\n') + i = 0 + i += 1 + f.write(f' {val:13.6f}') + f.write('\n\n\n') + comb_rule = " GEOM" if self.combining_rule == "geometric" else "" + f.write( + '\nNONBONDED nbxmod 5 atom cdiel fshift vatom vdistance vfswitch -\ncutnb 14.0 ' + f'ctofnb 12.0 ctonnb 10.0 eps 1.0 e14fac {1/scee} wmin 1.5{comb_rule}\n\n' + ) + for key, typ in self.atom_types.items(): + f.write(f'{key:<6s} {0:14.6f} {-abs(typ.epsilon):10.6f} {typ.rmin:14.6f}') + if typ.epsilon == typ.epsilon_14 and typ.rmin == typ.rmin_14: + f.write(f'{0:10.6f} {-abs(typ.epsilon) / scnb:10.6f} {typ.rmin:14.6f}\n') + else: + f.write(f'{0:10.6f} {-abs(typ.epsilon_14):10.6f} {typ.rmin_14:14.6f}\n') + f.write('\nEND\n') + +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +def _fit_IC_table(res, ictable): + """ + Determines cartesian coordinates from an internal coordinate table stored in + CHARMM residue topology files + + Parameters + ---------- + res : ResidueTemplate + The residue template for which coordinates are being determined + ictable : list[tuple(atoms, measurements)] + The internal coordinate table + + Notes + ----- + This method assigns an xx, xy, and xz attribute to ``res``. For the time + being, this is just a placeholder, as its functionality has not yet been + implemented (CHARMM does not use a 'traditional' Z-matrix, and I don't know + of any existing code that will compute the proper cartesian coordinates from + the form used by CHARMM) + """ + for atom in res: + atom.xx = atom.xy = atom.xz = 0.0 diff --git a/pmx/workflow/parmed/charmm/psf.py b/pmx/workflow/parmed/charmm/psf.py new file mode 100644 index 00000000..7766c03c --- /dev/null +++ b/pmx/workflow/parmed/charmm/psf.py @@ -0,0 +1,659 @@ +""" +Provides a Python class for parsing a PSF file and setting up a system +structure for it +""" +import re +import warnings +from copy import copy as _copy +from functools import wraps +from ..periodic_table import AtomicNum, element_by_mass +from ..topologyobjects import (Bond, Angle, Dihedral, Improper, AcceptorDonor, Group, Cmap, + UreyBradley, NoUreyBradley, Atom, DihedralType, ImproperType, + UnassignedAtomType) +from ..exceptions import CharmmError, CharmmWarning, ParameterError +from ..structure import needs_openmm, Structure +from ..utils.io import genopen + +def _catchindexerror(func): + """ + Protects a function from raising an index error, and replace that exception + with a CharmmError instead + """ + @wraps(func) + def newfunc(*args, **kwargs): + """ Catch the index error """ + try: + return func(*args, **kwargs) + except IndexError as e: + raise CharmmError(f'Array is too short: {e}') from e + + return newfunc + +class _FileEOF(Exception): + """ For control flow """ + +class _ZeroDict(dict): + """ + Contains a dict that returns dummy (zero) arguments when a key is not + present rather than raising a KeyError. The return value for non-existent + items is (0, []). It also special-case sections that have multiple pointers + to avoid index errors if those are not present in the PSF file + """ + def __getitem__(self, key): + try: + return dict.__getitem__(self, key) + except KeyError: + if key.startswith('NGRP'): + for k in self: + if k.startswith('NGRP'): + return dict.__getitem__(self, k) + return [0, 0], [] + elif key.startswith('NUMLP'): + for k in self: + if k.startswith('NUMLP'): + return dict.__getitem__(self, k) + return [0, 0], [] + return 0, [] + +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +_resre = re.compile(r'(-?\d+)([a-zA-Z]*)') + +class CharmmPsfFile(Structure): + """ + A chemical :class:`Structure` instantiated from CHARMM files. + + Parameters + ---------- + psf_name : str, optional + Name of the PSF file (it must exist) + + Raises + ------ + IOError : If file ``psf_name`` does not exist + CharmmPsfError : If any parsing errors are encountered + """ + @staticmethod + def _convert(string, type, message): + """ + Converts a string to a specific type, making sure to raise + CharmmError with the given message in the event of a failure. + + Parameters + ---------- + string : str + Input string to process + type : type + Type of data to convert to (e.g., ``int``) + message : str + Error message to put in exception if failed + """ + try: + return type(string) + except ValueError: + raise CharmmError(f'Could not convert {message} [{string}]') + + #=================================================== + + @classmethod + def _parse_psf_title_line(cls, line): + words = line[:line.index('!')].split() + title = line[line.index('!')+1:].strip().upper() + # Strip out description + if ':' in title: + title = title[:title.index(':')] + if len(words) == 1: + pointers = cls._convert(words[0], int, 'pointer') + else: + pointers = tuple([cls._convert(w, int, 'pointer') for w in words]) + return title, pointers + + #=================================================== + + @classmethod + def _parse_psf_section(cls, psf): + """ + This method parses a section of the PSF file + + Parameters + ---------- + psf : file + Open file that is pointing to the first line of the section that is + to be parsed + + Returns + ------- + title : str + The label of the PSF section we are parsing + pointers : (int/tuple of ints) + If one pointer is set, pointers is simply the integer that is value + of that pointer. Otherwise it is a tuple with every pointer value + defined in the first line + data : list + A list of all data in the parsed section converted to integers + """ + line = psf.readline() + while not line.strip(): + if not line: + raise _FileEOF('Unexpected EOF in PSF file') + else: + line = psf.readline() + if '!' in line: + title, pointers = cls._parse_psf_title_line(line) + else: + raise CharmmError('Could not determine section title') # pragma: no cover + line = psf.readline().strip() + if not line and title.startswith('NNB'): + # This will correctly handle the NNB section (which has a spurious + # blank line) as well as any sections that have 0 members. + line = psf.readline().strip() + # If this line has a title in it, then it's one of those weird PSF files that + # has basically no NNB section. Just skip over NNB and take the next section + # instead + if '!' in line: + title, pointers = cls._parse_psf_title_line(line) + line = psf.readline().strip() + data = [] + if title == 'NATOM' or title == 'NTITLE': + # Store these two sections as strings (ATOM section we will parse + # later). The rest of the sections are integer pointers + while line: + data.append(line) + line = psf.readline().strip() + else: + while line: + words = line.split() + data.extend([cls._convert(w, int, 'PSF data') for w in words]) + line = psf.readline().strip() + return title, pointers, data + + #=================================================== + + @_catchindexerror + def __init__(self, psf_name=None): + """ + Opens and parses a PSF file, then instantiates a CharmmPsfFile + instance from the data. + """ + from ..utils import tag_molecules + global _resre + Structure.__init__(self) + # Bail out if we don't have a filename + if psf_name is None: + return + # Open the PSF and read the first line. It must start with "PSF" + if isinstance(psf_name, str): + fileobj = genopen(psf_name, 'r') + own_handle = True + else: + fileobj = psf_name + own_handle = False + try: + self.name = psf_name if isinstance(psf_name, str) else '' + line = fileobj.readline() + if not line.startswith('PSF'): + raise CharmmError(f'Unrecognized PSF file. First line is {line.strip()}') + # Store the flags + psf_flags = line.split()[1:] + # Now get all of the sections and store them in a dict + fileobj.readline() + # Now get all of the sections + psfsections = _ZeroDict() + while True: + try: + sec, ptr, data = CharmmPsfFile._parse_psf_section(fileobj) + except _FileEOF: + break + psfsections[sec] = (ptr, data) + # store the title + self.title = psfsections['NTITLE'][1] + # Next is the number of atoms + natom = self._convert(psfsections['NATOM'][0], int, 'natom') + # Parse all of the atoms + for i in range(natom): + words = psfsections['NATOM'][1][i].split() + atid = int(words[0]) + if atid != i + 1: + raise CharmmError('Nonsequential atoms detected!') + segid = words[1] + rematch = _resre.match(words[2]) + if not rematch: + raise CharmmError(f'Could not interpret residue number {words[2]}') + resid, inscode = rematch.groups() + resid = self._convert(resid, int, 'residue number') + resname = words[3] + name = words[4] + attype = words[5] + # Try to convert the atom type to an integer a la CHARMM + try: + attype = int(attype) + except ValueError: + pass + charge = self._convert(words[6], float, 'partial charge') + mass = self._convert(words[7], float, 'atomic mass') + props = words[8:] + atom = Atom(name=name, type=attype, charge=charge, mass=mass, + atomic_number=AtomicNum[element_by_mass(mass)]) + atom.props = props + self.add_atom(atom, resname, resid, chain=segid, inscode=inscode, segid=segid) + # Now get the number of bonds + nbond = self._convert(psfsections['NBOND'][0], int, 'number of bonds') + if len(psfsections['NBOND'][1]) != nbond * 2: + raise CharmmError(f"Got {len(psfsections['NBOND'][1])} indexes for {nbond} bonds") + it = iter(psfsections['NBOND'][1]) + for i, j in zip(it, it): + self.bonds.append(Bond(self.atoms[i-1], self.atoms[j-1])) + # Now get the number of angles and the angle list + ntheta = self._convert(psfsections['NTHETA'][0], int, 'number of angles') + if len(psfsections['NTHETA'][1]) != ntheta * 3: + raise CharmmError(f"Got {len(psfsections['NTHETA'][1])} indexes for {ntheta} angles") + it = iter(psfsections['NTHETA'][1]) + for i, j, k in zip(it, it, it): + self.angles.append(Angle(self.atoms[i-1], self.atoms[j-1], self.atoms[k-1])) + self.angles[-1].funct = 5 # urey-bradley + # Now get the number of torsions and the torsion list + nphi = self._convert(psfsections['NPHI'][0], int, 'number of torsions') + if len(psfsections['NPHI'][1]) != nphi * 4: + raise CharmmError(f"Got {len(psfsections['NPHI'])} indexes for {nphi} torsions") + it = iter(psfsections['NPHI'][1]) + for i, j, k, l in zip(it, it, it, it): + self.dihedrals.append( + Dihedral(self.atoms[i-1], self.atoms[j-1], self.atoms[k-1], self.atoms[l-1]) + ) + self.dihedrals.split = False + # Now get the number of improper torsions + nimphi = self._convert(psfsections['NIMPHI'][0], int, 'number of impropers') + if len(psfsections['NIMPHI'][1]) != nimphi * 4: + raise CharmmError(f"Got {len(psfsections['NIMPHI'][1])} indexes for {nimphi} impropers") + it = iter(psfsections['NIMPHI'][1]) + for i, j, k, l in zip(it, it, it, it): + self.impropers.append( + Improper(self.atoms[i-1], self.atoms[j-1], self.atoms[k-1], self.atoms[l-1]) + ) + # Now handle the donors (what is this used for??) + ndon = self._convert(psfsections['NDON'][0], int, 'number of donors') + if len(psfsections['NDON'][1]) != ndon * 2: + raise CharmmError(f"Got {len(psfsections['NDON'][1])} indexes for {ndon} donors") + it = iter(psfsections['NDON'][1]) + for i, j in zip(it, it): + self.donors.append(AcceptorDonor(self.atoms[i-1], self.atoms[j-1])) + # Now handle the acceptors (what is this used for??) + nacc = self._convert(psfsections['NACC'][0], int, 'number of acceptors') + if len(psfsections['NACC'][1]) != nacc * 2: + raise CharmmError(f"Got {len(psfsections['NACC'][1])} indexes for {nacc} acceptors") + it = iter(psfsections['NACC'][1]) + for i, j in zip(it, it): + self.acceptors.append(AcceptorDonor(self.atoms[i-1], self.atoms[j-1])) + # Now get the group sections + try: + ngrp, nst2 = psfsections['NGRP NST2'][0] + except ValueError: # pragma: no cover + raise CharmmError('Could not unpack GROUP pointers') # pragma: no cover + tmp = psfsections['NGRP NST2'][1] + self.groups.nst2 = nst2 + # Now handle the groups + if len(psfsections['NGRP NST2'][1]) != ngrp * 3: + raise CharmmError(f"Got {len(tmp)} indexes for {ngrp} groups") + it = iter(psfsections['NGRP NST2'][1]) + for i, j, k in zip(it, it, it): + self.groups.append(Group(self.atoms[i], j, k)) + # Assign all of the atoms to molecules recursively + tmp = psfsections['MOLNT'][1] + tag_molecules(self) + molecule_list = [a.marked for a in self.atoms] + if len(tmp) == len(self.atoms): + # We have a CHARMM PSF file; now do NUMLP/NUMLPH sections + numlp, numlph = psfsections['NUMLP NUMLPH'][0] + if numlp != 0 or numlph != 0: + raise NotImplementedError( + 'Cannot currently handle PSFs with lone pairs defined in the NUMLP/' + 'NUMLPH section.' + ) + # Now do the CMAPs + ncrterm = self._convert(psfsections['NCRTERM'][0], int, 'Number of cross-terms') + if len(psfsections['NCRTERM'][1]) != ncrterm * 8: + raise CharmmError(f"Got {len(psfsections['NCRTERM'])} CMAP indexes for {ncrterm} cmap terms") + it = iter(psfsections['NCRTERM'][1]) + for i, j, k, l, m, n, o, p in zip(it, it, it, it, it, it, it, it): + self.cmaps.append( + Cmap.extended( + self.atoms[i - 1], + self.atoms[j - 1], + self.atoms[k - 1], + self.atoms[l - 1], + self.atoms[m - 1], + self.atoms[n - 1], + self.atoms[o - 1], + self.atoms[p - 1], + ) + ) + self.unchange() + self.flags = psf_flags + finally: + if own_handle: + fileobj.close() + + #=================================================== + + @classmethod + def from_structure(cls, struct, copy=False): + """ + Instantiates a CharmmPsfFile from an input Structure instance. This + method makes sure all atom types have uppercase-only names + + Parameters + ---------- + struct : :class:`parmed.structure.Structure` + The input structure to convert to a CharmmPsfFile instance + copy : bool, optional + If True, a copy of all items are made. Otherwise, the resulting + CharmmPsfFile is a shallow copy + + Returns + ------- + psf : :class:`CharmmPsfFile` + CHARMM PSF file + + Raises + ------ + ValueError if the functional form is not recognized or cannot be + implemented through the PSF and parameter/stream files + + Notes + ----- + If copy is False, the original object may have its atom type names + changed if any of them have lower-case letters + """ + from .parameters import _typeconv as typeconv + if (struct.rb_torsions or struct.trigonal_angles or + struct.out_of_plane_bends or struct.pi_torsions or + struct.stretch_bends or struct.torsion_torsions or + struct.chiral_frames or struct.multipole_frames or + struct.nrexcl != 3): + raise ValueError('Unsupported functional form for CHARMM PSF') + if copy: + struct = _copy(struct) + psf = cls() + psf.atoms = struct.atoms + psf.residues = struct.residues + psf.bonds = struct.bonds + psf.angles = struct.angles + psf.urey_bradleys = struct.urey_bradleys + psf.dihedrals = struct.dihedrals + psf.impropers = struct.impropers + psf.acceptors = struct.acceptors + psf.donors = struct.donors + psf.groups = struct.groups + psf.cmaps = struct.cmaps + + psf.bond_types = struct.bond_types + psf.angle_types = struct.angle_types + psf.urey_bradley_types = struct.urey_bradley_types + psf.dihedral_types = struct.dihedral_types + psf.improper_types = struct.improper_types + psf.cmap_types = struct.cmap_types + + for atom in psf.atoms: + atom.type = typeconv(atom.type) + if atom.atom_type is not UnassignedAtomType: + atom.atom_type.name = typeconv(atom.atom_type.name) + + # If no groups are defined, make each residue its own group + if not psf.groups: + for residue in psf.residues: + chg = sum(a.charge for a in residue) + if chg < 1e-4: + psf.groups.append(Group(residue[0], 1, 0)) + else: + psf.groups.append(Group(residue[0], 2, 0)) + psf.groups.nst2 = 0 + + return psf + + #=================================================== + + def __str__(self): + return self.name + + #=================================================== + + @needs_openmm + def createSystem(self, params=None, *args, **kwargs): + """ + Creates an OpenMM System object from the CHARMM PSF file. This is a + shortcut for calling `load_parameters` followed by + Structure.createSystem. If params is not None, `load_parameters` will be + called on that parameter set, and Structure.createSystem will be called + with the remaining args and kwargs + + Parameters + ---------- + params : CharmmParameterSet=None + If not None, this parameter set will be loaded + + See Also + -------- + :meth:`parmed.structure.Structure.createSystem` + In addition to `params`, this method also takes all arguments for + :meth:`parmed.structure.Structure.createSystem` + """ + if params is not None: + self.load_parameters(params) + return super().createSystem(*args, **kwargs) + + #=================================================== + + def load_parameters(self, parmset, copy_parameters=True): + """ + Loads parameters from a parameter set that was loaded via CHARMM RTF, + PAR, and STR files. + + Parameters + ---------- + parmset : :class:`CharmmParameterSet` + List of all parameters + + copy_parameters : bool, optional, default=True + If False, parmset will not be copied. + + WARNING: + ------- + Not copying parmset will cause ParameterSet and Structure to share + references to types. If you modify the original parameter set, the + references in Structure list_types will be silently modified. + However, if you change any reference in the parameter set, then that + reference will no longer be shared with structure. + + Example where the reference in ParameterSet is changed. The + following will NOT modify the parameters in the psf:: + + psf.load_parameters(parmset, copy_parameters=False) + parmset.angle_types[('a1', 'a2', a3')] = AngleType(1, 2) + + The following WILL change the parameter in the psf because the + reference has not been changed in ``ParameterSet``:: + + psf.load_parameters(parmset, copy_parameters=False) + a = parmset.angle_types[('a1', 'a2', 'a3')] + a.k = 10 + a.theteq = 100 + + Extra care should be taken when trying this with dihedral_types. + Since dihedral_type is a Fourier sequence, ParameterSet stores + DihedralType for every term in DihedralTypeList. Therefore, the + example below will STILL modify the type in the :class:`Structure` + list_types:: + + parmset.dihedral_types[('a', 'b', 'c', 'd')][0] = DihedralType(1, 2, 3) + + This assigns a new instance of DihedralType to an existing + DihedralTypeList that ParameterSet and Structure are tracking and + the shared reference is NOT changed. + + Use with caution! + + Notes + ----- + - If any dihedral or improper parameters cannot be found, I will try + inserting wildcards (at either end for dihedrals and as the two + central atoms in impropers) and see if that matches. Wild-cards will + apply ONLY if specific parameters cannot be found. + + - This method will expand the dihedrals attribute by adding a separate + Dihedral object for each term for types that have a multi-term + expansion + + Raises + ------ + ParameterError if any parameters cannot be found + """ + if copy_parameters: + parmset = _copy(parmset) + self.combining_rule = parmset.combining_rule + # First load the atom types + for atom in self.atoms: + try: + if isinstance(atom.type, int): + atype = parmset.atom_types_int[atom.type] + else: + atype = parmset.atom_types_str[atom.type.upper()] + except KeyError: + raise ParameterError(f"Could not find atom type for {atom.type}") + atom.atom_type = atype + # Change to string type to look up the rest of the parameters. Use + # upper-case since all parameter sets were read in as upper-case + atom.type = str(atom.atom_type).upper() + atom.atomic_number = atype.atomic_number + + # Next load all of the bonds + for bond in self.bonds: + # Construct the key + key = (min(bond.atom1.type, bond.atom2.type), + max(bond.atom1.type, bond.atom2.type)) + try: + bond.type = parmset.bond_types[key] + except KeyError: + raise ParameterError(f"Missing bond type for {bond}") + bond.type.used = False + # Build the bond_types list + del self.bond_types[:] + for bond in self.bonds: + if bond.type.used: + continue + bond.type.used = True + self.bond_types.append(bond.type) + bond.type.list = self.bond_types + # Next load all of the angles. If a Urey-Bradley term is defined for + # this angle, also build the urey_bradley and urey_bradley_type lists + del self.urey_bradleys[:] + for ang in self.angles: + # Construct the key + key = (min(ang.atom1.type, ang.atom3.type), + ang.atom2.type, + max(ang.atom1.type, ang.atom3.type)) + try: + ang.type = parmset.angle_types[key] + ang.type.used = False + ubt = parmset.urey_bradley_types[key] + if ubt is not NoUreyBradley: + ub = UreyBradley(ang.atom1, ang.atom3, ubt) + self.urey_bradleys.append(ub) + ubt.used = False + except KeyError: + raise ParameterError(f"Missing angle type for {ang}") + del self.urey_bradley_types[:] + del self.angle_types[:] + for ub in self.urey_bradleys: + if ub.type.used: + continue + ub.type.used = True + self.urey_bradley_types.append(ub.type) + ub.type.list = self.urey_bradley_types + for ang in self.angles: + if ang.type.used: + continue + ang.type.used = True + self.angle_types.append(ang.type) + ang.type.list = self.angle_types + # Next load all of the dihedrals. + active_dih_list = set() + for dih in self.dihedrals: + # Store the atoms + a1, a2, a3, a4 = dih.atom1, dih.atom2, dih.atom3, dih.atom4 + key = (a1.type, a2.type, a3.type, a4.type) + # First see if the exact dihedral is specified + if not key in parmset.dihedral_types: + # Check for wild-cards + key = ('X', a2.type, a3.type, 'X') + if not key in parmset.dihedral_types: + raise ParameterError(f'No dihedral parameters found for {dih}') + dih.type = parmset.dihedral_types[key] + dih.type.used = False + pair = (dih.atom1.idx, dih.atom4.idx) # To determine exclusions + if (dih.atom1 in dih.atom4.bond_partners or + dih.atom1 in dih.atom4.angle_partners): + dih.ignore_end = True + elif pair in active_dih_list: + dih.ignore_end = True + else: + active_dih_list.add(pair) + active_dih_list.add((dih.atom4.idx, dih.atom1.idx)) + del self.dihedral_types[:] + for dihedral in self.dihedrals: + if dihedral.type.used: + continue + dihedral.type.used = True + self.dihedral_types.append(dihedral.type) + dihedral.type.list = self.dihedral_types + # Now do the impropers + for imp in self.impropers: + a1, a2, a3, a4 = imp.atom1.type, imp.atom2.type, imp.atom3.type, imp.atom4.type + imp.type = parmset.match_improper_type(a1, a2, a3, a4) + if imp.type is None: + raise ParameterError(f"No improper type for {a1}, {a2}, {a3}, and {a4}") + imp.type.used = False + # prepare list of harmonic impropers present in system + del self.improper_types[:] + for improper in self.impropers: + if improper.type.used: + continue + improper.type.used = True + if isinstance(improper.type, ImproperType): + self.improper_types.append(improper.type) + improper.type.list = self.improper_types + elif isinstance(improper.type, DihedralType): + self.dihedral_types.append(improper.type) + improper.type.list = self.dihedral_types + else: + assert False, 'Should not be here' + # Look through the list of impropers -- if there are any periodic + # impropers, move them over to the dihedrals list + for i in reversed(range(len(self.impropers))): + if isinstance(self.impropers[i].type, DihedralType): + imp = self.impropers.pop(i) + dih = Dihedral(imp.atom1, imp.atom2, imp.atom3, imp.atom4, + improper=True, ignore_end=True, type=imp.type) + imp.delete() + self.dihedrals.append(dih) + # Now do the cmaps. These will not have wild-cards + for cmap in self.cmaps: + key = (cmap.atom1.type, cmap.atom2.type, cmap.atom3.type, + cmap.atom4.type, cmap.atom2.type, cmap.atom3.type, + cmap.atom4.type, cmap.atom5.type) + try: + cmap.type = parmset.cmap_types[key] + except KeyError: + raise ParameterError(f"No CMAP parameters found for {cmap}") + cmap.type.used = False + del self.cmap_types[:] + for cmap in self.cmaps: + if cmap.type.used: continue + cmap.type.used = True + self.cmap_types.append(cmap.type) + cmap.type.list = self.cmap_types + + #=================================================== + + def clear_cmap(self): + " Clear the cmap list to prevent any CMAP parameters from being used " + del self.cmaps[:] diff --git a/pmx/workflow/parmed/constants.py b/pmx/workflow/parmed/constants.py new file mode 100644 index 00000000..13a12cd4 --- /dev/null +++ b/pmx/workflow/parmed/constants.py @@ -0,0 +1,100 @@ +""" +List of all pointers and constants used in the Amber topology file. + +Can be used like: + from parmed.constants import * +""" +from enum import IntEnum + +from math import pi as _pi, sqrt as _sqrt, log10 as _log10, acos as _acos + +__all__ = ['AMBER_ELECTROSTATIC', 'AMBER_POINTERS', 'PrmtopPointers', + 'RAD_TO_DEG', 'DEG_TO_RAD', 'TRUNCATED_OCTAHEDRON_ANGLE'] + +AMBER_ELECTROSTATIC = 18.2223 +CHARMM_ELECTROSTATIC = _sqrt(332.0716) + +AMBER_POINTERS = """ +NATOM : total number of atoms +NTYPES : total number of distinct atom types +NBONH : number of bonds containing hydrogen +MBONA : number of bonds not containing hydrogen +NTHETH : number of angles containing hydrogen +MTHETA : number of angles not containing hydrogen +NPHIH : number of dihedrals containing hydrogen +MPHIA : number of dihedrals not containing hydrogen +NHPARM : currently not used +NPARM : currently not used +NEXT : number of excluded atoms +NRES : number of residues +NBONA : MBONA + number of constraint bonds +NTHETA : MTHETA + number of constraint angles +NPHIA : MPHIA + number of constraint dihedrals +NUMBND : number of unique bond types +NUMANG : number of unique angle types +NPTRA : number of unique dihedral types +NATYP : number of atom types in parameter file, see SOLTY below +NPHB : number of distinct 10-12 hydrogen bond pair types +IFPERT : set to 1 if perturbation info is to be read in +NBPER : number of bonds to be perturbed +NGPER : number of angles to be perturbed +NDPER : number of dihedrals to be perturbed +MBPER : number of bonds with atoms completely in perturbed group +MGPER : number of angles with atoms completely in perturbed group +MDPER : number of dihedrals with atoms completely in perturbed groups +IFBOX : set to 1 if standard periodic box, 2 when truncated octahedral +NMXRS : number of atoms in the largest residue +IFCAP : set to 1 if the CAP option from edit was specified +NUMEXTRA: number of extra points +NCOPY : Number of copies for advanded simulations +""" +# These global variables provide a more natural way of accessing +# the various pointers. Most useful if they're loaded into the +# top-level namespace. +class PrmtopPointers(IntEnum): + NATOM = 0 + NTYPES = 1 + NBONH = 2 + MBONA = 3 + NTHETH = 4 + MTHETA = 5 + NPHIH = 6 + MPHIA = 7 + NHPARM = 8 + NPARM = 9 + NEXT = 10 + NRES = 11 + NBONA = 12 + NTHETA = 13 + NPHIA = 14 + NUMBND = 15 + NUMANG = 16 + NPTRA = 17 + NATYP = 18 + NPHB = 19 + IFPERT = 20 + NBPER = 21 + NGPER = 22 + NDPER = 23 + MBPER = 24 + MGPER = 25 + MDPER = 26 + IFBOX = 27 + NMXRS = 28 + IFCAP = 29 + NUMEXTRA = 30 + NCOPY = 31 + NNB = NEXT + +RAD_TO_DEG = 180.0 / _pi +DEG_TO_RAD = _pi / 180.0 +TRUNCATED_OCTAHEDRON_ANGLE = _acos(-1/3) * 180 / _pi + +# For use in floating point comparisons +TINY = 1.0e-8 +SMALL = 1.0e-4 +TINY_DIGITS = int(_log10(TINY) + 0.5) +SMALL_DIGITS = int(_log10(SMALL) + 0.5) + +# For I/O +DEFAULT_ENCODING = 'UTF-8' diff --git a/pmx/workflow/parmed/dlpoly/__init__.py b/pmx/workflow/parmed/dlpoly/__init__.py new file mode 100644 index 00000000..621faf7d --- /dev/null +++ b/pmx/workflow/parmed/dlpoly/__init__.py @@ -0,0 +1,7 @@ +""" +Contains classes for parsing DLPOLY topology and parameter files +""" +__all__ = ['DlpolyFieldFile', 'DlpolyConfigFile'] + +from ..dlpoly.dlpolyfield import DlpolyFieldFile +from ..dlpoly.dlpolyconfig import DlpolyConfigFile diff --git a/pmx/workflow/parmed/dlpoly/dlpolyconfig.py b/pmx/workflow/parmed/dlpoly/dlpolyconfig.py new file mode 100644 index 00000000..1e52f602 --- /dev/null +++ b/pmx/workflow/parmed/dlpoly/dlpolyconfig.py @@ -0,0 +1,138 @@ +""" +This module contains functionality relevant to loading and parsing DLPOLY CONFIG +(coordinate) files and building a stripped-down Structure from it +""" +from ..constants import TINY +from ..formats.registry import FileFormatType +from ..geometry import box_lengths_and_angles_to_vectors, reduce_box_vectors +from ..utils.io import genopen + +class DlpolyConfigFile(metaclass=FileFormatType): + """ Writes Dlpoly CONFIG files """ + #=================================================== + + @staticmethod + def write(struct, dest, precision=12, combine=False): + """ Write a Dlpoly CONFIG File from a Structure + + Parameters + ---------- + struct : :class:`Structure` + The structure to write to a Dlpoly CONFIG file (must have coordinates) + dest : str or file-like + The name of a file or a file object to write the Dlpoly CONFIG file to + precision : int, optional + The number of decimal places to print in the coordinates. Default 9 + combine : 'all', None, or list of iterables, optional + Equivalent to the combine argument of the DlpolyFieldFile.write + method. If None, system atom order may be changed to meet the need + for contiguously bonded groups of atoms to be part of a single + moleculetype. All other values leave the atom order unchanged. + Default is None. + """ + + def _write_atom_line(atom, atid, resid, has_vels, dest, precision): + varwidth = 8 + precision + crdfmt = '%%%d.%df' % (varwidth, precision) + velfmt = '%%%d.%df' % (varwidth, precision) + dest.write('%-8s %6d' % (atom.type[:5], atid)) + dest.write('\n') + dest.write((crdfmt % (atom.xx))[:varwidth]) + dest.write((crdfmt % (atom.xy))[:varwidth]) + dest.write((crdfmt % (atom.xz))[:varwidth]) + dest.write('\n') + if has_vels: + dest.write((velfmt % (atom.vx))[:varwidth]) + dest.write((velfmt % (atom.vy))[:varwidth]) + dest.write((velfmt % (atom.vz))[:varwidth]) + dest.write('\n') + + own_handle = False + if isinstance(dest, str): + dest = genopen(dest, 'w') + own_handle = True + elif not hasattr(dest, 'write'): + raise TypeError('dest must be a file name or file-like object') + + dest.write('CONFIGURATION\n') + # CONFIG file key (do we have velocities as well?) + has_vels = all(hasattr(a, 'vx') for a in struct.atoms) + if has_vels: + dest.write(' 1') + else: + dest.write(' 0') + # Periodic Boundary Key + if struct.box is not None: + a, b, c = reduce_box_vectors(*box_lengths_and_angles_to_vectors(*struct.box)) + if all([abs(x-90) < TINY for x in struct.box[3:]]): + if (a[0] == b[1] and b[1] == c[2]): + dest.write(' 1') + else: + dest.write(' 2') + else: + dest.write(' 3') + else: + dest.write(' 0') + dest.write(' %5d 0.0000000000E+00\n' % len(struct.atoms)) + # Box information + if struct.box is not None: + varwidth = 8 + precision + boxfmt = '%%%d.%df%%%d.%df%%%d.%df\n' % (varwidth, precision, varwidth, precision, varwidth, precision) + dest.write(boxfmt % (a[0], a[1], a[2])) + dest.write(boxfmt % (b[0], b[1], b[2])) + dest.write(boxfmt % (c[0], c[1], c[2])) + if combine != 'all': + resid, atid = 0, 0 + # use struct.split to get residue order as per topology file + split_struct = struct.split() + n_mols = sum(len(mol[1]) for mol in split_struct) + unused_atoms = list(struct.atoms) + for molid in range(n_mols): + # loop through molids so we can get the correct molecule + # according to the order they appear + molecule = [ + mol[0] for mol in split_struct if molid in mol[1]][0] + new_molecule = set() # track atoms added + last_found_atom = None # track when CONFIG and FIELD diverge + + for residue in molecule.residues: + resid += 1 + for atom in residue.atoms: + # for each atom in split topology get the first + # matching occurrence in the original structure + for original_atom in unused_atoms: + if atom.type == original_atom.type and \ + atom.name == original_atom.name and \ + atom.residue.name == original_atom.residue.name: + + if last_found_atom is not None and \ + original_atom.idx != last_found_atom.idx + 1: + # a rearrangement has occurred! Need to do + # extra check that we've found the correct + # original_atom + if len(new_molecule.intersection( + original_atom.bond_partners)) == 0: + # original_atom must be bonded to at + # least one atom in the molecule we + # are currently writing otherwise find + # next candidate + continue + + atid += 1 + _write_atom_line( + original_atom, atid % 100000, + resid % 100000, has_vels, dest, precision) + new_molecule.add(original_atom) + last_found_atom = original_atom + unused_atoms.remove(original_atom) + break + else: + raise RuntimeError("Could not find %s" % atom) + else: + for atom in struct.atoms: + resid = (atom.residue.idx + 1) % 100000 + atid = (atom.idx + 1) % 100000 + _write_atom_line(atom, atid, resid, has_vels, dest, precision) + + if own_handle: + dest.close() diff --git a/pmx/workflow/parmed/dlpoly/dlpolyfield.py b/pmx/workflow/parmed/dlpoly/dlpolyfield.py new file mode 100644 index 00000000..2ba939cf --- /dev/null +++ b/pmx/workflow/parmed/dlpoly/dlpolyfield.py @@ -0,0 +1,395 @@ +""" +This module contains functionality relevant to building a DLPOLY topology file +""" +from collections import defaultdict +import copy +import math + +from ..exceptions import DlpolyError +from ..formats.registry import FileFormatType +from ..gromacs.gromacstop import _Defaults, _diff_diheds, _gettype, TopFromStructureMixin +from ..parameters import ParameterSet +from ..structure import Structure +from ..topologyobjects import ExtraPoint, DihedralType, DihedralTypeList, UnassignedAtomType +from ..utils.io import genopen + + +# Dlpoly uses "funct" flags in its parameter files to indicate what kind of +# functional form is used for each of its different parameter types. This is +# taken from the topdirs.c source code file along with a table in the Dlpoly +# user manual. The table below summarizes my findings, for reference: + +# Bonds +# ----- +# 1 - F_BONDS : simple harmonic potential +# 2 - F_G96BONDS : fourth-power potential +# 3 - F_MORSE : morse potential +# 4 - F_CUBICBONDS : cubic potential +# 5 - F_CONNBONDS : not even implemented in DLPOLY +# 6 - F_HARMONIC : seems to be the same as (1) ?? +# 7 - F_FENEBONDS : finietely-extensible-nonlinear-elastic (FENE) potential +# 8 - F_TABBONDS : bond function from tabulated function +# 9 - F_TABBONDSNC : bond function from tabulated function (no exclusions) +# 10 - F_RESTRBONDS : restraint bonds + +# Angles +# ------ +# 1 - F_ANGLES : simple harmonic potential +# 2 - F_G96ANGLES : cosine-based angle potential +# 3 - F_CROSS_BOND_BONDS : bond-bond cross term potential +# 4 - F_CROSS_BOND_ANGLES : bond-angle cross term potential +# 5 - F_UREY_BRADLEY : Urey-Bradley angle-bond potential +# 6 - F_QUARTIC_ANGLES : 4th-order polynomial potential +# 7 - F_TABANGLES : angle function from tabulated function +# 8 - F_LINEAR_ANGLES : angle function from tabulated function +# 9 - F_RESTRANGLES : restricted bending potential + +# Dihedrals +# --------- +# 1 - F_PDIHS : periodic proper torsion potential [ k(1+cos(n*phi-phase)) ] +# 2 - F_IDIHS : harmonic improper torsion potential +# 3 - F_RBDIHS : Ryckaert-Bellemans torsion potential +# 4 - F_PIDIHS : periodic harmonic improper torsion potential (same as 1) +# 5 - F_FOURDIHS : Fourier dihedral torsion potential +# 8 - F_TABDIHS : dihedral potential from tabulated function +# 9 - F_PDIHS : Same as 1, but can be multi-term +# 10 - F_RESTRDIHS : Restricted torsion potential +# 11 - F_CBTDIHS : combined bending-torsion potential + +class DlpolyFieldFile(Structure, TopFromStructureMixin, metaclass=FileFormatType): + """ Class providing a writer for a DLPOLY topology file + """ + + #=================================================== + + def __init__(self): + super().__init__() + self.parameterset = None + self.defaults = _Defaults(gen_pairs='yes') # make ParmEd's default yes + + #=================================================== + + def write(self, dest, combine=None, parameters='inline'): + """ Write a Dlpoly Topology File from a Structure + + Parameters + ---------- + dest : str or file-like + The name of a file or a file object to write the Dlpoly topology to + combine : 'all', None, or list of iterables, optional + If None, no molecules are combined into a single moleculetype. If + 'all', all molecules are combined into a single moleculetype. + Otherwise, the list of molecule indices (start from 0) will control + which atoms are combined into single moleculetype's. Default is None + parameters : 'inline' or str or file-like object, optional + This specifies where parameters should be printed. If 'inline' + (default), the parameters are written on the same lines as the + valence terms are defined on. Any other string is interpreted as a + filename for an ITP that will be written to and then included at the + top of `dest`. If it is a file-like object, parameters will be + written there. If parameters is the same as ``dest``, then the + parameter types will be written to the same topologyfile. + + Raises + ------ + ValueError if the same molecule number appears in multiple combine lists + TypeError if the dest input cannot be parsed + ValueError if the combine, or parameters input cannot be parsed + """ + own_handle = False + fname = '' + params = ParameterSet.from_structure(self, allow_unequal_duplicates=True) + if isinstance(dest, str): + fname = '%s ' % dest + dest = genopen(dest, 'w') + own_handle = True + elif not hasattr(dest, 'write'): + raise TypeError('dest must be a file name or file-like object') + + # Determine where to write the parameters + own_parfile_handle = False + if parameters == 'inline': + parfile = dest + elif isinstance(parameters, str): + if parameters == fname.strip(): + parfile = dest + else: + own_parfile_handle = True + parfile = genopen(parameters, 'w') + elif hasattr(parameters, 'write'): + parfile = parameters + else: + raise ValueError('parameters must be "inline", a file name, or ' + 'a file-like object') + + # Determine where to write the molecules + own_molfile_handle = False + _molfile = dest + + # Error-checking for combine + if combine is not None: + if isinstance(combine, str): + if combine.lower() != 'all': + raise ValueError('combine must be None, list of indices, or "all"') + else: + combine_lists = [] + for indices in combine: + indices = sorted(set(indices)) + if any((indices[i+1] - indices[i]) != 1 for i in range(len(indices)-1)): + raise ValueError('Can only combine adjacent molecules') + combine_lists.append(indices) + try: + # Write the header + if self.title: + dest.write(self.title+"\n") + else: + dest.write('AMBER_SYSTEM\n') # pragma: no cover + dest.write('UNITS kcal\n') + if combine is None: + molecules = self.split() + dest.write('MOLECULAR types %d\n'%(len(molecules))) + sysnum = 1 + names = [] + nameset = set() + for molecule, num in molecules: + if len(molecule.residues) == 1: + title = molecule.residues[0].name + if title in nameset: + orig = title + sfx = 2 + while title in nameset: + title = '%s%d' % (orig, sfx) + sfx += 1 + else: + title = 'system%d' % sysnum + sysnum += 1 + names.append(title) + nameset.add(title) + DlpolyFieldFile._write_molecule(molecule, _molfile, + title, len(num), params, + parameters == 'inline') + elif isinstance(combine, str) and combine.lower() == 'all': + dest.write('MOLECULAR types 1\n') + DlpolyFieldFile._write_molecule(self, _molfile, 'system', 1, + params, parameters == 'inline') + # Molecules + dest.write('[ molecules 2 ]\n; Compound #mols\n') + dest.write('%-15s %6d\n' % ('system', 1)) + else: + molecules = self.split() + nmols = sum(len(m[1]) for m in molecules) + moleculedict = dict() + # Hash our molecules by indices + for m, num in molecules: + for i in num: + moleculedict[i] = m + combined_molecules = [] + for cl in combine_lists: + counts = defaultdict(int) + mols_in_mol = [] + for molid in cl: + try: + mol = moleculedict[molid] + except KeyError: + raise IndexError('Molecule ID out of range') + counts[id(moleculedict[molid])] += 1 + if counts[id(moleculedict[molid])] == 1: + mols_in_mol.append(mol) + if counts[id(mols_in_mol[0])] > 1: + combmol = mols_in_mol[0] * counts[id(mols_in_mol[0])] + else: + combmol = copy.copy(mols_in_mol[0]) + for i, mol in enumerate(mols_in_mol): + if i == 0: continue + assert id(mol) in counts and counts[id(mol)] > 0 + if counts[id(mol)] > 1: + combmol += mol * counts[id(mol)] + else: + combmol += mol + combined_molecules.append((combmol, cl[0], len(cl))) + nmols -= (len(cl) - 1) + # combined_molecules now contains a list of tuples, and that + # tuple stores the combined molecule, first molecule index of + # the pre-combined molecule, and how many molecules were + # combined + + # Sort combined molecules by starting location + combined_molecules.sort(key=lambda x: x[1]) + new_molecules = [] + counts = defaultdict(set) + cmc = 0 # Combined Molecule Counter + add = 0 # How many molecules to "skip" due to combining + for i in range(nmols): + ii = i + add + if (cmc < len(combined_molecules) and + combined_molecules[cmc][1] == ii): + new_molecules.append([combined_molecules[cmc][0], set([i])]) + add += combined_molecules[cmc][2] - 1 + cmc += 1 + elif len(counts[id(moleculedict[ii])]) == 0: + counts[id(moleculedict[ii])].add(i) + new_molecules.append([moleculedict[ii], counts[id(moleculedict[ii])]]) + else: + counts[id(moleculedict[ii])].add(i) + sysnum = 1 + names = [] + nameset = set() + dest.write('MOLECULAR types %d\n'%(len(new_molecules))) + for molecule, num in new_molecules: + if len(molecule.residues) == 1: + title = molecule.residues[0].name + if title in nameset: + orig = title + sfx = 2 + while title in nameset: + title = '%s%d' % (orig, sfx) + sfx += 1 + else: + title = 'system%d' % sysnum + sysnum += 1 + names.append(title) + nameset.add(title) + DlpolyFieldFile._write_molecule( + molecule, _molfile, title, len(num), params, parameters == 'inline' + ) + finally: + # Print all VdW parameters for the full system + nattyps = len(params.atom_types) + parfile.write('VDW %d\n'%(int(nattyps*(nattyps+1)/2))) + cnt1 = 0 + for key1, atom_type1 in params.atom_types.items(): + cnt1 += 1 + cnt2 = 0 + for key2, atom_type2 in params.atom_types.items(): + cnt2 += 1 + if (cnt2 < cnt1): continue + eps = math.sqrt(atom_type1.epsilon*atom_type2.epsilon) + sig = (atom_type1.sigma+atom_type2.sigma)/2 + parfile.write('%-8s%-8s%4s %14.8f %14.8f\n'% + (atom_type1, atom_type2, 'lj', eps, sig)) + # Close statement + dest.write('CLOSE\n') + # Close handles + if own_handle: + dest.close() + if own_parfile_handle: + parfile.close() + if own_molfile_handle: + _molfile.close() + + #=================================================== + + @staticmethod + def _write_molecule(struct, dest, title, nmols, params, writeparams): + # Printing molecule header + dest.write('%s\n' % (title)) + dest.write('NUMMOLS %d\n'%(nmols)) + # Getting number of atoms + natoms = 0 + for residue in struct.residues: + for atom in residue: + natoms += 1 + # Print masses and charges + dest.write('ATOMS %d\n'%(natoms)) + for residue in struct.residues: + for atom in residue: + dest.write('%-8s %10.6f %12.8f 1\n' % (atom.type, atom.mass, atom.charge)) + # Do valence terms now + EPs = [a for a in struct.atoms if isinstance(a, ExtraPoint)] + if len(struct.atoms) - len(EPs) == 3: + try: + oxy, = (a for a in struct.atoms if a.atomic_number == 8) + hyd1, hyd2 = (a for a in struct.atoms if a.atomic_number == 1) + except ValueError: + pass + # Print bonds + if struct.bonds: + conv = 2.0 + dest.write('BONDS %d\n'%(len(struct.bonds))) + for bond in struct.bonds: + if (isinstance(bond.atom1, ExtraPoint) or isinstance(bond.atom2, ExtraPoint)): + continue # pragma: no cover + # bond.funct ==1 is for a simple harmonic potential + if (bond.funct != 1): + raise DlpolyError('Bond between atoms %d and %d is of an invalid type!' + % (bond.atom1.idx+1, bond.atom2.idx+1)) + dest.write('%4s %6d %6d' % ('harm', bond.atom1.idx+1, bond.atom2.idx+1)) + if bond.type is None: + dest.write('\n') + continue # pragma: no cover + key = (_gettype(bond.atom1), _gettype(bond.atom2)) + if writeparams or key not in params.bond_types or bond.type != params.bond_types[key]: + dest.write(' %12.6f %12.6f %12.6f %12.6f' % (bond.type.k*conv, bond.type.req, 0.0, 0.0)) + dest.write('\n') + # Angles + if struct.angles: + conv = 2.0 + dest.write('ANGLES %d\n'%(len(struct.angles))) + for angle in struct.angles: + # angle.funct ==1 is for a simple harmonic potential + if (angle.funct != 1): + raise DlpolyError('Angle between atoms %d, %d and %d is of an invalid type!' + % (angle.atom1.idx+1, angle.atom2.idx+1, + angle.atom3.idx+1)) + dest.write('%4s %6d %6d %6d' % ('harm', angle.atom1.idx+1, angle.atom2.idx+1, + angle.atom3.idx+1)) + if angle.type is None: + dest.write('\n') + continue + key = (_gettype(angle.atom1), _gettype(angle.atom2), _gettype(angle.atom3)) + param_equal = params.angle_types.get(key) == angle.type + if writeparams or not param_equal: + dest.write(' %12.7f %12.7f %12.7f %12.7f' % (angle.type.k*conv, + angle.type.theteq, 0.0, 0.0)) + dest.write('\n') + # Dihedrals + if struct.dihedrals: + dest.write('DIHEDRALS %d\n'%(len(struct.dihedrals))) + conv = 1.0 + for dihed in struct.dihedrals: + # dihed.funct ==1 or 4 is for a simple harmonic potential + if (dihed.funct != 1 and dihed.funct != 4): + raise DlpolyError('Dihedral between atoms %d, %d, %d and %d is of an invalid type!' + % (dihed.atom1.idx+1, dihed.atom2.idx+1, + dihed.atom3.idx+1, dihed.atom4.idx+1)) + dest.write('%-4s %6d %6d %6d %6d' % ('cos', dihed.atom1.idx+1, dihed.atom2.idx+1, + dihed.atom3.idx+1, dihed.atom4.idx+1)) + if dihed.type is None: + dest.write('\n') + continue + if dihed.improper: + typedict = params.improper_periodic_types + else: + typedict = params.dihedral_types + key = (_gettype(dihed.atom1), _gettype(dihed.atom2), + _gettype(dihed.atom3), _gettype(dihed.atom4)) + if writeparams or key not in typedict or _diff_diheds(dihed.type, typedict[key]): + scee = 0.0 + if (dihed.type.scee > 0.0): scee = 1.0/dihed.type.scee + scnb = 0.0 + if (dihed.type.scnb > 0.0): scnb = 1.0/dihed.type.scnb + dest.write(' %12.7f %12.7f %4d %10.5f %10.5f' % (dihed.type.phi_k*conv, + dihed.type.phase, int(dihed.type.per), scee, scnb)) + dest.write('\n') + # Impropers + if struct.impropers: + dest.write('INVERSIONS %d\n'%(len(struct.impropers))) + conv = 2.0 + for dihed in struct.impropers: + # dihed.funct ==1 is for a simple harmonic potential + if (dihed.funct != 1): + raise DlpolyError('Dihedral between atoms %d, %d, %d and %d is of an invalid type!' + % (dihed.atom1.idx+1, dihed.atom2.idx+1, + dihed.atom3.idx+1, dihed.atom4.idx+1)) + dest.write('%-4s %6d %6d %6d %6d' % ('harm', dihed.atom1.idx+1, + dihed.atom2.idx+1, dihed.atom3.idx+1, + dihed.atom4.idx+1)) + if dihed.type is None: + dest.write('\n') + continue + # BUGBUG: We always write improper types since we don't + # currently store the correct ordering of the types in the + # improper section + dest.write(' %12.7f %12.7f\n' % (dihed.type.psi_k*conv, dihed.type.psi_eq)) + # Finish + dest.write('FINISH\n') diff --git a/pmx/workflow/parmed/entos/__init__.py b/pmx/workflow/parmed/entos/__init__.py new file mode 100644 index 00000000..dd7e214f --- /dev/null +++ b/pmx/workflow/parmed/entos/__init__.py @@ -0,0 +1,5 @@ +""" Bindings to generate Entos objects from ParmEd objects """ +from .converters import to_entos_molecule, to_entos_qmmm_system +from .imports import HAS_ENTOS, HAS_MISTRAL + +__all__ = ['to_entos_molecule', 'to_entos_qmmm_system', 'HAS_ENTOS', 'HAS_MISTRAL'] diff --git a/pmx/workflow/parmed/entos/converters.py b/pmx/workflow/parmed/entos/converters.py new file mode 100644 index 00000000..b2fa246f --- /dev/null +++ b/pmx/workflow/parmed/entos/converters.py @@ -0,0 +1,116 @@ +""" Factory functions that will generate Entos Molecule and QMMMInput from ParmEd Structure objects """ +from typing import Optional, Sequence, Union + +import numpy as np + +from ..structure import Structure +from ..utils.decorators import needs_openmm +from .imports import Molecule, constants, QMMMSystem, HAS_ENTOS, HAS_MISTRAL + +def to_entos_molecule( + struct: Structure, + charge: Optional[float] = None, + multiplicity: Optional[int] = None, +) -> Molecule: + """ + Create an Entos Molecule object from a Structure + + Parameters + ---------- + struct: :class:`Structure` + The structure from which a Molecule should be created + charge: float = None + The net charge on the molecule. If it is None, the charge is determined as the sum + of partial atomic charges on each atom in the structure + multiplicity: int = None + The spin-multiplicity on the structure. If not set, the lowest multiplicity with the + given number of electrons will be used + + Returns + ------- + Molecule + The molecule given the input structure with coordinates + """ + if not HAS_ENTOS: + raise ImportError("to_entos_molecule will not work without installing Entos Sierra") + + if struct.coordinates is None: + raise ValueError("Cannot create an Entos Molecule without coordinates") + + if charge is None: + charge = sum(a.charge for a in struct.atoms) + + return Molecule( + atomic_numbers=[a.atomic_number for a in struct.atoms], + geometry=struct.coordinates * constants.cf("angstrom", "bohr"), + multiplicity=multiplicity, + charge=charge, + ) + +@needs_openmm +def to_entos_qmmm_system( + struct: Structure, + qm_selection: Union[str, Sequence[int]], + charge: Optional[float] = None, + multiplicity: Optional[int] = None, + **kwargs, +) -> QMMMSystem: + """ + Create a multiscale input for QM/MM simulations with Qcore from a Structure object + + Parameters + ---------- + struct: :class:`Structure` + The input structure to create the multiscale input from + qm_selection: :class:`Sequence` or str + The selection array (starting from 0), mask array (if length matches + struct atom count), or atom mask syntax (if selection is a string) for the + QM atoms in the system + charge: float = None + The charge on the QM region. If not set, the value is taken from the sum of + the charges on the qm_selection + multiplicity: int = None + The multiplicity on the QM region. The default is taken as the minimum multiplicity + given the number of electrons in the QM region. + **kwargs + Additional keyword arguments are passed to the `createSystem` call when creating an OpenMM + System object for doing QM/MM calculations + + Returns + ------- + multiscale_input: QMMMInput + The QM/MM input + """ + from openmm.app import LJPME, NoCutoff + if not HAS_MISTRAL: + raise ImportError("You must install Entos mistral to create a QMMMInput object") + if struct.coordinates is None: + raise ValueError("Cannot create a QMMMSystem with no coordinates") + selection_array = _selection_to_array(struct, qm_selection) + if selection_array[0] < 0 or selection_array[-1] >= len(struct.atoms): + raise ValueError("QM selection array is out of bounds") + create_system_args = {"nonbondedMethod": NoCutoff if struct.box is None else LJPME} + create_system_args.update(kwargs) + system = struct.createSystem(**create_system_args) + if charge is None: + charge = sum(struct.atoms[i].charge for i in selection_array) + return QMMMSystem( + geometry=struct.coordinates * constants.cf("angstrom", "bohr"), + atomic_numbers=[a.atomic_number for a in struct.atoms], + forcefield=system, + topology=struct.topology, + qm_indices=selection_array, + qm_charge=charge, + qm_multiplicity=multiplicity, + ) + +def _selection_to_array(struct: Structure, qm_selection: Union[str, Sequence[int]]) -> np.ndarray: + """ Converts a selection into an array of indices """ + if isinstance(qm_selection, str): + from ..amber import AmberMask + mask = AmberMask(struct, qm_selection) + return np.array(list(mask.Selected())) + elif len(qm_selection) == len(struct.atoms): + return np.array([i for i, mask in enumerate(qm_selection) if mask]) + else: + return np.array(sorted(list(set(qm_selection)))) diff --git a/pmx/workflow/parmed/entos/imports.py b/pmx/workflow/parmed/entos/imports.py new file mode 100644 index 00000000..4b3f513d --- /dev/null +++ b/pmx/workflow/parmed/entos/imports.py @@ -0,0 +1,20 @@ +""" Imports from the Entos ecosystem """ + +__all__ = ['sierra', 'Molecule', 'QMMMInput', 'HAS_MISTRAL', 'HAS_ENTOS', 'constants'] + +try: + import sierra + from sierra import constants + from sierra.inputs import Molecule + try: + from sierra.inputs import QMMMInput, QMMMSystem + except ImportError: + HAS_MISTRAL = False + QMMMInput = QMMMSystem = None + else: + HAS_MISTRAL = True +except ImportError: + sierra = Molecule = constants = QMMMInput = QMMMSystem = None + HAS_ENTOS = HAS_MISTRAL = False +else: + HAS_ENTOS = True diff --git a/pmx/workflow/parmed/exceptions.py b/pmx/workflow/parmed/exceptions.py new file mode 100644 index 00000000..1802b78f --- /dev/null +++ b/pmx/workflow/parmed/exceptions.py @@ -0,0 +1,120 @@ +""" +This module contains all of the exceptions that are used in the chemistry +package +""" +import warnings + +# Hard errors + +class ParmedError(Exception): + """ Base class for all non-trivial exceptions raised by ParmEd """ + +class ParsingError(ParmedError): + """ If there was a problem parsing any kind of file """ + +class PDBError(ParsingError): + """ If there was a problem parsing a PDB file """ + +class Mol2Error(ParsingError): + """ If there was a problem parsing a Mol2 file """ + +class MaskError(ParmedError): + """ Error when a Mask is poorly formed """ + +class OpenMMError(ParmedError): + """ If there's a problem making an OpenMM system """ + +class AmberError(ParsingError): + """ This is a generic AmberParmError """ + +class TinkerError(ParsingError): + """ Raised when one of the TINKER parsing routines hits a bad file """ + +class CharmmError(ParsingError): + """ If there is a problem parsing CHARMM PSF files """ + +class ResidueError(ParmedError): + """ For when there are problems defining a residue """ + +class IncompatiblePatchError(ParmedError): + """ For when applying a PatchTemplate to a ResidueTemplate fails """ + +class ParameterError(ParmedError): + """ If a parameter is missing from a database """ + +class GromacsError(ParmedError): + """ If there is a problem parsing GROMACS topology files """ + +class DlpolyError(ParmedError): + """ If there is a problem parsing DLPOLY topology files """ + +class FormatNotFound(ParmedError): + """ If the file format does not have a registered parser with it """ + +class RosettaError(ParmedError): + """ If there is a problem loading a Rosetta pose object """ + +class PreProcessorError(ParmedError): + """ If there is a problem running the C-like preprocessor """ + +class MoleculeError(ParmedError): + """ If there is a problem defining a molecule via the bond graph """ + +class PdbxError(ParmedError): + """ Class for catching general errors with PDBx/mmCIF parsing """ + +class PdbxSyntaxError(PdbxError): + """ Class for catching errors in mmCIF/PDBx syntax """ + def __init__(self, lineNumber='-1', text=''): + Exception.__init__(self) + self.lineNumber = lineNumber + self.text = text + + def __str__(self): + return "%%ERROR - [at line: %d] %s" % (self.lineNumber, self.text) + +class InputError(ParmedError): + """ When there is an error with input """ + +# Warnings + +class ParmedWarning(Warning): + """ Base class for all warnings raised by ParmEd """ + +# Make sure that all warnings are always printed +warnings.filterwarnings('always', category=ParmedWarning) + +class PDBWarning(ParmedWarning): + """ A non-fatal error to indicate a problematic PDB file """ + +class AmberWarning(ParmedWarning): + """ If there is something that is non-fatal """ + +class SplitResidueWarning(ParmedWarning): + """ For if a residue with the same number but different names is split """ + +class CharmmWarning(ParmedWarning): + """ For non-fatal PSF parsing issues """ + +class GromacsWarning(ParmedWarning): + " If we are uncertain about something regarding the GROMACS topology file " + +class DlpolyWarning(ParmedWarning): + " If we are uncertain about something regarding the DLPOLY topology file " + +class ParameterWarning(ParmedWarning): + """ If a type of parameter is missing, but you don't want it to be fatal """ + +class TinkerWarning(ParmedWarning): + pass + +class PreProcessorWarning(ParmedWarning): + """ If there is something we should warn about in preprocessing """ + +class OpenMMWarning(ParmedWarning): + """ If there is something we should warn when processing OpenMM objects """ + +# Control flow exceptions + +class CharmmPsfEOF(ParmedError): + """ If we hit an end-of-file in parsing CHARMM files """ diff --git a/pmx/workflow/parmed/formats/__init__.py b/pmx/workflow/parmed/formats/__init__.py new file mode 100644 index 00000000..d13c30ff --- /dev/null +++ b/pmx/workflow/parmed/formats/__init__.py @@ -0,0 +1,20 @@ +""" +A package dealing with different file formats and automatic detection of those +formats +""" + +__all__ = ['load_file', 'PDBFile', 'CIFFile', 'Mol2File', 'PSFFile', 'PQRFile', 'SDFFile'] + +from .registry import load_file +from .mol2 import Mol2File +from .pdb import PDBFile, CIFFile +from .pqr import PQRFile +from .psf import PSFFile +from .sdf import SDFFile + +# Now let's modify structure.Structure and add our write methods from our +# various formats +from ..structure import Structure +Structure.write_pdb = PDBFile.write +Structure.write_cif = CIFFile.write +Structure.write_psf = PSFFile.write diff --git a/pmx/workflow/parmed/formats/mol2.py b/pmx/workflow/parmed/formats/mol2.py new file mode 100644 index 00000000..07baf701 --- /dev/null +++ b/pmx/workflow/parmed/formats/mol2.py @@ -0,0 +1,630 @@ +""" +This module contains parsers for mol2-format files (with support for the mol3 +extension described at http://q4md-forcefieldtools.org/Tutorial/leap-mol3.php +""" +import copy +from contextlib import closing +from typing import Tuple, Optional, List, Any + +from ..exceptions import Mol2Error, ParameterWarning +from ..modeller.residue import ResidueTemplate, ResidueTemplateContainer +from ..periodic_table import element_by_name, AtomicNum +from ..residue import AminoAcidResidue, RNAResidue, DNAResidue +from ..structure import Structure +from ..topologyobjects import Atom, Bond +from ..utils.io import genopen +from .registry import FileFormatType +import warnings + +__all__ = ['Mol2File'] + +class Mol2File(metaclass=FileFormatType): + """ Class to read and write TRIPOS Mol2 files """ + + BOND_ORDER_MAP = dict(ar=1.5, am=1.25) + REVERSE_BOND_ORDER_MAP = {val: key for key, val in BOND_ORDER_MAP.items()} + + @staticmethod + def id_format(filename): + """ Identify the file as a Mol2 (or Mol3) file format or not + + Parameters + ---------- + filename : str + Name of the file to test whether or not it is a mol2 file + + Returns + ------- + is_fmt : bool + True if it is a mol2 (or mol3) file, False otherwise + """ + with closing(genopen(filename, 'r')) as f: + for line in f: + if line.startswith('#') or not line.strip(): + continue + return line.startswith('@') + return False + + @classmethod + def parse(cls, filename, structure=False): + """ Parses a mol2 file (or mol3) file + + Parameters + ---------- + filename : str or file-like + Name of the file to parse or file-like object to parse from + structure : bool, optional + If True, the return value is a :class:`Structure` instance. If + False, it is either a :class:`ResidueTemplate` or + :class:`ResidueTemplateContainter` instance, depending on whether + there is one or more than one residue defined in it. Default is + False + + Returns + ------- + molecule : :class:`Structure`, :class:`ResidueTemplate`, or + :class:`ResidueTemplateContainer` + The molecule defined by this mol2 file + + Raises + ------ + Mol2Error + If the file format is not recognized or non-numeric values are + present where integers or floating point numbers are expected. Also + raises Mol2Error if you try to parse a mol2 file that has multiple + @ entries with ``structure=True``. + """ + if isinstance(filename, str): + f = genopen(filename, 'r') + own_handle = True + else: + f = filename + own_handle = False + rescont = ResidueTemplateContainer() + struct = Structure() + restemp = ResidueTemplate() + mol_info = [] + multires_structure = False + try: + section = None + last_residue = None + headtail = 'head' + molecule_number = 0 + for line in f: + if line.startswith('#') or (not line.strip() and section is None): + continue + if line.startswith('@'): + section = line[9:].strip() + if section == 'MOLECULE' and (restemp.atoms or rescont): + if structure: + raise Mol2Error('Structure cannot have more than 1 @') + # Set the residue name from the MOL2 title if the + # molecule had only 1 residue and it was given a name in + # the title + if not multires_structure and mol_info[0]: + restemp.name = mol_info[0] + multires_structure = False + rescont.append(restemp) + restemp = ResidueTemplate() + struct = Structure() + last_residue = None + molecule_number += 1 + mol_info = [] + continue + if section is None: + raise Mol2Error('Bad mol2 file format') + if section == 'MOLECULE': + cls._parse_molecule_line(line, mol_info) + continue + if section == 'ATOM': + atom, (resid, resname) = cls._parse_atom_line(line, mol_info) + struct.add_atom(atom, resname, resid) + if last_residue is None: + last_residue = (resid, resname) + restemp.name = resname + elif last_residue != (resid, resname): + rescont.append(restemp) + restemp = ResidueTemplate() + restemp.name = resname + last_residue = (resid, resname) + multires_structure = True + try: + restemp.add_atom(copy.copy(atom)) + except ValueError: + # Allow mol2 files being parsed as a Structure to have duplicate atom names + if not structure: + raise + continue + if section == 'BOND': + cls._parse_bond_line( + line, struct, rescont, restemp, structure, multires_structure + ) + continue + if section == 'CRYSIN': + box = cls._parse_box_line(line) + struct.box = copy.copy(box) + rescont.box = copy.copy(box) + continue + if section == 'SUBSTRUCTURE': + substructure = cls._parse_substructure_line(line) + if substructure is None: + continue + # Set the chain ID + for res in struct.residues: + if res.number == substructure[0] and res.name == substructure[1]: + res.chain = substructure[2] + continue + # MOL3 sections + if section == 'HEADTAIL': + headtail = cls._parse_mol3_line(line, rescont, restemp, headtail) + continue + if section == 'RESIDUECONNECT': + cls._parse_rescnct_line(line, rescont, restemp) + continue + + def assign_atomic_numbers(atoms): + for atom in atoms: + atomic_number = _guess_atomic_number(atom.name, restemp) + if atomic_number == 0: + atomic_number = _guess_atomic_number(atom.type, restemp) + atom.atomic_number = atomic_number + + if structure: + assign_atomic_numbers(struct.atoms) + return struct + elif len(rescont) > 0: + if not multires_structure and mol_info[0]: + restemp.name = mol_info[0] + rescont.append(restemp) + for res in rescont: + assign_atomic_numbers(res.atoms) + return rescont + else: + assign_atomic_numbers(restemp.atoms) + return restemp + except ValueError as err: + raise Mol2Error(f'String conversion trouble: {err}') from err + finally: + if own_handle: + f.close() + + @staticmethod + def write(struct, dest, mol3=False, split=False, compress_whitespace=False): + """ Writes a mol2 file from a structure or residue template + + Parameters + ---------- + struct : :class:`Structure` or :class:`ResidueTemplate` or + :class:`ResidueTemplateContainer` + The input structure to write the mol2 file from + dest : str or file-like obj + Name of the file to write or open file handle to write to + mol3 : bool, optional + If True and ``struct`` is a ResidueTemplate or container, write + HEAD/TAIL sections. Default is False + split : bool, optional + If True and ``struct`` is a ResidueTemplateContainer or a Structure + with multiple residues, each residue is printed in a separate + @ section that appear sequentially in the output file + compress_whitespace : bool, optional + If True, seprate fields on one line with a single space instead of + aligning them with whitespace. This is useful for parsers that + truncate lines at 80 characters (e.g., some versions of OpenEye). + However, it will not look as "neat" upon visual inspection in a text + editor. Default is False. + """ + own_handle = False + if not hasattr(dest, 'write'): + own_handle = True + dest = genopen(dest, 'w') + if split: + # Write sequentially if it is a multi-residue container or Structure + if isinstance(struct, ResidueTemplateContainer): + try: + for res in struct: + Mol2File.write(res, dest, mol3, compress_whitespace=compress_whitespace) + finally: + if own_handle: dest.close() + return + elif isinstance(struct, Structure) and len(struct.residues) > 1: + try: + for res in ResidueTemplateContainer.from_structure(struct): + Mol2File.write(res, dest, mol3, compress_whitespace=compress_whitespace) + finally: + if own_handle: dest.close() + return + try: + if isinstance(struct, ResidueTemplateContainer): + natom = sum([len(c) for c in struct]) + # To find the number of bonds, we need to total number of bonds + # + the number of bonds that would be formed by "stitching" + # together residues via their head and tail + bonds = [] + charges = [] + bases = [1 for res in struct] + for i, res in enumerate(struct): + if i < len(struct) - 1: + bases[i+1] = bases[i] + len(res) + for i, res in enumerate(struct): + for bond in res.bonds: + bonds.append((bond.atom1.idx+bases[i], bond.atom2.idx+bases[i], bond.order)) + if i < len(struct)-1 and (res.tail is not None and + struct[i+1].head is not None): + bonds.append((res.tail.idx+bases[i], struct[i+1].head.idx+bases[i+1], bond.order)) + charges.extend([a.charge for a in res]) + residues = struct + name = struct.name or struct[0].name + else: + natom = len(struct.atoms) + bonds = [(b.atom1.idx+1, b.atom2.idx+1, b.order) for b in struct.bonds] + if isinstance(struct, ResidueTemplate): + residues = [struct] + name = struct.name + else: + residues = struct.residues + name = struct.residues[0].name + charges = [a.charge for a in struct.atoms] + dest.write('@MOLECULE\n') + dest.write(f'{name}\n') + dest.write(f'{natom:d} {len(bonds):d} {len(residues):d} 0 1\n') + if len(residues) == 1: + dest.write('SMALL\n') + else: + for residue in residues: + if AminoAcidResidue.has(residue.name): + dest.write('PROTEIN\n') + break + if RNAResidue.has(residue.name) or DNAResidue.has(residue.name): + dest.write('NUCLEIC\n') + break + else: + dest.write('BIOPOLYMER\n') + if not any(charges): + dest.write('NO_CHARGES\n') + printchg = False + else: + dest.write('USER_CHARGES\n') + printchg = True + # See if we want to print box info + if hasattr(struct, 'box') and struct.box is not None: + box = struct.box + dest.write('@CRYSIN\n') + if compress_whitespace: + fmt = '{0:.4f} {1:.4f} {2:.4f} {3:.4f} {4:.4f} {5:.4f} 1 1\n' + else: + fmt = '{0:10.4f} {1:10.4f} {2:10.4f} {3:10.4f} {4:10.4f} {5:10.4f} 1 1\n' + dest.write(fmt.format(*box)) + # Now do ATOM section + dest.write('@ATOM\n') + j = 1 + for i, res in enumerate(residues): + for atom in res: + try: + x = atom.xx + except AttributeError: + x = 0 + try: + y = atom.xy + except AttributeError: + y = 0 + try: + z = atom.xz + except AttributeError: + z = 0 + if compress_whitespace: + fmt = '{0:d} {1:s} {2:.4f} {3:.4f} {4:.4f} {5:s} {6:d} {7:s}' + else: + fmt = '{0:8d} {1:<8s} {2:10.4f} {3:10.4f} {4:10.4f} {5:<8s} {6:6d} {7:<8s}' + dest.write(fmt.format(j, atom.name, x, y, z, atom.type.strip() or atom.name, i+1, res.name)) + if printchg: + if compress_whitespace: + fmt = ' {0:.6f}\n' + else: + fmt = ' {0:10.6f}\n' + dest.write(fmt.format(atom.charge)) + else: + dest.write('\n') + j += 1 + dest.write('@BOND\n') + for i, bond in enumerate(bonds): + if bond[2] in Mol2File.REVERSE_BOND_ORDER_MAP: + order = Mol2File.REVERSE_BOND_ORDER_MAP[bond[2]] + else: + order = int(bond[2]) + if compress_whitespace: + fmt = '{0:d} {1:d} {2:d} {3:s}\n' + else: + fmt = '{0:8d} {1:8d} {2:8d} {3:s}\n' + dest.write(fmt.format(i+1, bond[0], bond[1], str(order))) + dest.write('@SUBSTRUCTURE\n') + first_atom = 0 + for i, res in enumerate(residues): + if not hasattr(res, 'chain') or not res.chain: + chain = '****' + else: + chain = res.chain + intresbonds = 0 + if isinstance(res, ResidueTemplate): + if i != len(residues)-1 and (res.tail is not None and residues[i+1].head is not None): + intresbonds += 1 + if i != 0 and (res.head is not None and residues[i-1].tail is not None): + intresbonds += 1 + else: + for atom in res: + for a2 in atom.bond_partners: + if a2.residue is not res: + intresbonds += 1 + if compress_whitespace: + fmt = '{0:d} {1:s} {2:d} RESIDUE {3:d} {4:s} ROOT {5:d}\n' + else: + fmt = '{0:8d} {1:<8s} {2:8d} RESIDUE {3:4d} {4:<4s} ROOT {5:6d}\n' + dest.write(fmt.format(i+1, res.name, first_atom+1, 0, chain[:4], intresbonds)) + first_atom += len(res) + if mol3: + dest.write('@HEADTAIL\n') + for i, res in enumerate(residues): + if isinstance(res, ResidueTemplate): + if res.head is not None: + dest.write(f'{res.head.name} {i + 1}\n') + else: + dest.write('0 0\n') + if res.tail is not None: + dest.write(f'{res.tail.name} {i + 1}\n') + else: + dest.write('0 0\n') + else: + head = tail = None + for atom in res: + for a2 in atom.bond_partners: + if a2.residue.idx == res.idx - 1: + head = atom + if a2.residue.idx == res.idx + 1: + tail = atom + if head is not None: + dest.write(f'{head.name} {i + 1}\n') + else: + dest.write('0 0\n') + if tail is not None: + dest.write(f'{tail.name} {i + 1}\n') + else: + dest.write('0 0\n') + dest.write('@RESIDUECONNECT\n') + for i, res in enumerate(residues): + if isinstance(res, ResidueTemplate): + con = [res.head, res.tail, None, None, None, None] + for i, a in enumerate(res.connections): + con[i+2] = a + else: + con = [None, None, None, None, None, None] + ncon = 2 + for atom in res: + for a2 in atom.bond_partners: + if a2.residue.idx == res.idx - 1: + con[0] = atom + elif a2.residue.idx == res.idx + 1: + con[1] = atom + elif a2.residue.idx != res.idx: + con[ncon] = atom + ncon += 1 + dest.write(str(i + 1)) + for a in con: + if a is not None: + dest.write(f' {a.name}') + else: + dest.write(' 0') + dest.write('\n') + finally: + if own_handle: + dest.close() + + @staticmethod + def _parse_molecule_line(line: str, mol_info: List[Any]) -> None: + # Section formatted as follows: + # mol_name + # num_atoms [num_bonds [num_substr [num_feat [num_sets]]]] + # mol_type + # charge_type + # [status_bits] + # [mol_comment] + # TODO: Do something with the name. + if len(mol_info) == 0: + mol_info.append(line.strip()) + elif len(mol_info) == 1: + mol_info.append([int(x) for x in line.split()]) + elif len(mol_info) == 2: + mol_info.append(line.strip()) + elif len(mol_info) == 3: + mol_info.append(line.strip()) + # Ignore the rest + + # Parser helpers + @staticmethod + def _parse_atom_line(line: str, mol_info: List[Any]) -> Tuple[Atom, Tuple[int, str]]: + # Section formatted as follows: + # atom_id -- serial number of atom + # atom_name -- name of the atom + # x -- X-coordinate of the atom + # y -- Y-coordinate of the atom + # z -- Z-coordinate of the atom + # atom_type -- type of the atom + # subst_id -- Residue serial number + # subst_name -- Residue name + # charge -- partial atomic charge + # status_bit -- ignored + words = line.split() + resid = 0 + idx = int(words[0]) + name = words[1] + xyz = [float(words[i]) for i in range(2, 5)] + typ = words[5] + resid = 0 if len(words) < 7 else int(words[6]) + resname = "UNK" if len(words) < 8 else words[7] + charge = 0.0 if len(words) < 9 or "NO_CHARGES" in mol_info else float(words[8]) + atom = Atom(name=name, type=typ, number=idx, charge=charge) + atom.xx, atom.xy, atom.xz = xyz + return atom, (resid, resname) + + @classmethod + def _parse_bond_line( + cls, + line: str, + struct: Structure, + rescont: ResidueTemplateContainer, + restemp: ResidueTemplate, + structure: bool, + multires_structure: bool, + ) -> None: + # Section formatted as follows: + # bond_id -- serial number of bond (ignored) + # origin_atom_id -- serial number of first atom in bond + # target_atom_id -- serial number of other atom in bond + # bond_type -- string describing bond type + # status_bits -- ignored + words = line.split() + int(words[0]) # Bond serial number... redundant and ignored + a1 = int(words[1]) + a2 = int(words[2]) + order = 1.0 if len(words) < 4 else words[3] + if order in cls.BOND_ORDER_MAP: + order = cls.BOND_ORDER_MAP[order] + else: + try: + order = float(order) + except ValueError: + warnings.warn(f'Bond order not recognized: {order}', ParameterWarning) + order = 1.0 + atom1 = struct.atoms.find_original_index(a1) + atom2 = struct.atoms.find_original_index(a2) + struct.bonds.append(Bond(atom1, atom2, order=order)) + # Now add it to our residue container + # See if it's a head/tail connection + if atom1.residue is not atom2.residue: + if atom1.residue.idx == len(rescont): + res1 = restemp + elif atom1.residue.idx < len(rescont): + res1 = rescont[atom1.residue.idx] + # assert atom1.residue.idx <= len(rescont), 'Bad bond!' + if atom2.residue.idx == len(rescont): + res2 = restemp + elif atom2.residue.idx < len(rescont): + res2 = rescont[atom2.residue.idx] + # assert atom2.residue.idx <= len(rescont), 'Bad bond!' + # assert res1 is not res2, 'BAD identical residues' + idx1 = atom1.idx - atom1.residue[0].idx + idx2 = atom2.idx - atom2.residue[0].idx + if atom1.residue.idx < atom2.residue.idx: + res1.tail = res1[idx1] + res2.head = res2[idx2] + else: + res1.head = res1[idx1] + res2.tail = res2[idx2] + elif not multires_structure: + if not structure: + restemp.add_bond(a1-1, a2-1, order) + else: + # Same residue, add the bond + offset = atom1.residue[0].idx + if atom1.residue.idx == len(rescont): + res = restemp + else: + res = rescont[atom1.residue.idx] + res.add_bond(atom1.idx-offset, atom2.idx-offset, order) + + @staticmethod + def _parse_box_line(line: str) -> List[float]: + # Section formatted as follows: + # a -- length of first unit cell vector + # b -- length of second unit cell vector + # c -- length of third unit cell vector + # alpha -- angle b/w b and c + # beta -- angle b/w a and c + # gamma -- angle b/w a and b + # space group -- number of space group (ignored) + # space group setting -- ignored + words = line.split() + box = [float(w) for w in words[:6]] + if len(box) != 6: + raise ValueError(f'{len(box)} box dimensions found; needed 6') + return box + + @staticmethod + def _parse_substructure_line(line: str) -> Optional[Tuple[int, str, str]]: + # Section formatted as follows: + # subst_id -- residue number + # subst_name -- residue name + # root_atom -- first atom of residue + # subst_type -- ignored (usually 'RESIDUE') + # dict_type -- type of substructure (ignored) + # chain -- chain ID of residue + # sub_type -- type of the chain + # inter_bonds -- # of inter-substructure bonds + # status -- ignored + # comment -- ignored + words = line.split() + if not words: + return None + return int(words[0]), words[1], '' if len(words) < 6 else words[5] + + @staticmethod + def _parse_mol3_line( + line: str, rescont: ResidueTemplateContainer, restemp: ResidueTemplate, headtail: str + ) -> str: + atname, residx = line.split() + residx = int(residx) + if residx in (0, 1) or residx - 1 == len(rescont): + res = restemp + elif residx - 1 < len(rescont): + res = rescont[residx-1] + else: + raise Mol2Error('Residue out of range in head/tail') + for atom in res: + if atom.name == atname: + if headtail == 'head': + res.head = atom + headtail = 'tail' + else: + res.tail = atom + headtail = 'head' + break + else: + if headtail == 'head': + headtail = 'tail' + else: + headtail = 'head' + return headtail + + @staticmethod + def _parse_rescnct_line( + line: str, rescont: ResidueTemplateContainer, restemp: ResidueTemplate + ) -> None: + words = line.split() + residx = int(words[0]) + if residx - 1 == len(rescont): + res = restemp + elif residx - 1 < len(rescont): + res = rescont[residx-1] + else: + raise Mol2Error('Residue out of range in residueconnect') + for a in words[3:]: + if a == '0': + continue + for atom in res: + if atom.name == a: + res.connections.append(atom) + break + else: + raise Mol2Error(f'Residue connection atom {a} not found in residue {residx}') + + + +def _guess_atomic_number(name, residue=None): + """ Guesses the atomic number """ + # Special-case single-atom residues, which are almost always ions + name = ''.join(c for c in name if c.isalpha()) + if residue is None or len(residue.atoms) == 1: + if len(name) > 1: + try: + return AtomicNum[name[0].upper() + name[1].lower()] + except KeyError: + return AtomicNum[element_by_name(name)] + return AtomicNum[element_by_name(name)] diff --git a/pmx/workflow/parmed/formats/pdb.py b/pmx/workflow/parmed/formats/pdb.py new file mode 100644 index 00000000..64d72506 --- /dev/null +++ b/pmx/workflow/parmed/formats/pdb.py @@ -0,0 +1,1760 @@ +""" +This package contains classes responsible for reading and writing both PDB and +PDBx/mmCIF files. +""" +from collections import OrderedDict, namedtuple +from contextlib import closing +from string import ascii_letters +import io +import ftplib +import gzip +import numpy as np +from ..exceptions import PDBError, PDBWarning +from ..formats.pdbx import PdbxReader, PdbxWriter, containers +from ..formats.registry import FileFormatType +from ..periodic_table import AtomicNum, Mass, Element, element_by_name +from ..residue import AminoAcidResidue, RNAResidue, DNAResidue, WATER_NAMES +from ..modeller import StandardBiomolecularResidues +from ..structure import Structure +from ..topologyobjects import Atom, ExtraPoint, Bond, Link +from ..symmetry import Symmetry +from ..utils.io import genopen +import re +import warnings + +#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +_ascii_letters_set = set(ascii_letters) + +def _compare_atoms(old_atom, new_atom, resname, resid, chain, segid, inscode): + """ + Compares two atom instances, along with the residue name, number, and chain + identifier, to determine if two atoms are actually the *same* atom, but + simply different conformations + + Parameters + ---------- + old_atom : :class:`Atom` + The original atom that has been added to the structure already + new_atom : :class:`Atom` + The new atom that we want to see if it is the same as the old atom + resname : ``str`` + The name of the residue that the new atom would belong to + resid : ``int`` + The number of the residue that the new atom would belong to + chain : ``str`` + The chain identifier that the new atom would belong to + segid : ``str`` + The segment identifier for the molecule + inscode : ``str`` + The insertion code for the residue + + Returns + ------- + True if they are the same atom, False otherwise + """ + if old_atom.name != new_atom.name: return False + if old_atom.residue.name != resname: return False + if old_atom.residue.number != resid: return False + if old_atom.residue.chain != chain.strip(): return False + if old_atom.residue.segid != segid.strip(): return False + if old_atom.residue.insertion_code != inscode.strip(): return False + return True + +#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +def _standardize_resname(resname): + """ Looks up a standardized residue name for the given resname """ + try: + return AminoAcidResidue.get(resname, abbronly=True).abbr, False + except KeyError: + try: + return RNAResidue.get(resname).abbr, False + except KeyError: + try: + return DNAResidue.get(resname).abbr, False + except KeyError: + if resname.strip() in WATER_NAMES: + return 'HOH', True + else: + return resname, True + +def _is_hetatm(resname): + """ Sees if residue name is "standard", otherwise, we need to use HETATM to + print atom records instead of ATOM + """ + if len(resname) != 3: + return not (RNAResidue.has(resname) or DNAResidue.has(resname)) + return not (AminoAcidResidue.has(resname) or RNAResidue.has(resname) or DNAResidue.has(resname)) + +def _number_truncated_to_n_digits(num, digits): + """ Truncates the given number to the specified number of digits """ + if num < 0: + return int(-(-num % eval(f'1e{digits - 1:d}'))) + return int(num % eval(f'1e{digits:d}')) + +#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +class PDBFile(metaclass=FileFormatType): + """ Standard PDB file format parser and writer """ + #=================================================== + + AtomLookupKey = namedtuple( + 'AtomLookupKey', ('name', 'number', 'residue_name', 'residue_number', 'chain', + 'insertion_code', 'segment_id', 'alternate_location') + ) + + @staticmethod + def id_format(filename): + """ Identifies the file type as a PDB file + + Parameters + ---------- + filename : str or file object + Name of the file to check format for + + Returns + ------- + is_fmt : bool + True if it is a PDB file + """ + if isinstance(filename, str): + own_handle = True + fileobject = genopen(filename, 'r') + elif hasattr(filename, 'read'): + own_handle = False + fileobject = filename + + try: + for line in fileobject: + if line[:6] in {'CRYST1', 'END ', 'END', 'HEADER', 'NUMMDL', + 'MASTER', 'AUTHOR', 'CAVEAT', 'COMPND', 'EXPDTA', + 'MDLTYP', 'KEYWDS', 'OBSLTE', 'SOURCE', 'SPLIT ', + 'SPRSDE', 'TITLE ', 'ANISOU', 'CISPEP', 'CONECT', + 'DBREF ', 'HELIX ', 'HET ', 'LINK ', 'MODRES', + 'REVDAT', 'SEQADV', 'SHEET ', 'SSBOND', 'FORMUL', + 'HETNAM', 'HETSYN', 'SEQRES', 'SITE ', 'ENDMDL', + 'MODEL ', 'TER ', 'JRNL ', 'REMARK', 'TER', 'DBREF ', + 'DBREF2', 'DBREF1', 'DBREF', 'HET', 'LINKR '}: + continue + # Hack to support reduce-added flags + elif line[:6] == 'USER ' and line[6:9] == 'MOD': + continue + elif line[:5] in ('ORIGX', 'SCALE', 'MTRIX'): + if line[5] not in '123': + return False + elif line[:6] in ('ATOM ', 'HETATM'): + atnum, atname = line[6:11], line[12:16] + resname, resid = line[17:20], line[22:26] + x, y, z = line[30:38], line[38:46], line[46:54] + occupancy, bfactor = line[54:60], line[60:66] + elem = line[76:78] + # Check for various attributes. This is the first atom, so + # we can assume we haven't gotten into the regime of "weird" + # yet, like hexadecimal atom/residue indices. + if not atnum.strip().lstrip('-').isdigit(): return False + if atname.strip().isdigit(): return False + if not resname.strip(): return False + if not resid.strip().lstrip('-').isdigit(): return False + try: + float(x), float(y), float(z) + except ValueError: + return False + if occupancy.strip(): + try: + float(occupancy) + except ValueError: + return False + if bfactor.strip(): + try: + float(bfactor) + except ValueError: + return False + if elem.strip(): + if any(x.isdigit() for x in elem): + return False + return True + else: + return False + return False + finally: + if own_handle: + fileobject.close() + + #=================================================== + + def __init__(self, fileobj): + # Open file object that we are parsing + self.fileobj = fileobj + + self._atom_map_from_attributes = OrderedDict() + self._atom_map_from_all_attributes = OrderedDict() + self._current_model_number = 1 + self._residue_indices_overflow = False + self._atom_indices_overflow = False + self.struct = Structure() + self._symmetry_lines = [] + self._link_lines = [] + self._coordinates = [[]] + self._model_atom_counts = [0] + self._anisou_records = dict() + self._atom_map_from_atom_number = dict() + self._atom_map_to_parent = dict() + self._model1_atoms_in_structure = set() + self._model_open = True + self._insertion_codes = set() # Only permitted for compliant PDB files + self._last_atom = None + self._last_residue = None + # Some writers use additional fields to hold extra digits for the residue number if it goes + # more than 4 digits + self._residue_number_field_extended_by = 0 + # Fallback if parser fails to process residue numbers + self._last_residue_number_label = None + + #=================================================== + + @staticmethod + def download(pdb_id, timeout=10, saveto=None): + """ + Goes to the wwPDB website and downloads the requested PDB, loading it + as a :class:`Structure` instance + + Parameters + ---------- + pdb_id : str + The 4-letter PDB ID to try and download from the RCSB PDB database + timeout : float, optional + The number of seconds to wait before raising a timeout error. + Default is 10 seconds + saveto : str, optional + If provided, this will be treated as a file name to which the PDB + file will be saved. If None (default), no PDB file will be written. + This will be a verbatim copy of the downloaded PDB file, unlike the + somewhat-stripped version you would get by using + :meth:`Structure.write_pdb ` + + Returns + ------- + struct : :class:`Structure ` + Structure instance populated by the requested PDB + + Raises + ------ + socket.timeout if the connection times out while trying to contact the + FTP server + + IOError if there is a problem retrieving the requested PDB or writing a + requested ``saveto`` file + + ImportError if the gzip module is not available + + TypeError if pdb_id is not a 4-character string + """ + if not isinstance(pdb_id, str) or len(pdb_id) != 4: + raise ValueError('pdb_id must be the 4-letter PDB code') + + pdb_id = pdb_id.lower() + ftp = ftplib.FTP('ftp.wwpdb.org', timeout=timeout) + ftp.login() + fileobj = io.BytesIO() + try: + ftp_loc = f"/pub/pdb/data/structures/divided/pdb/{pdb_id[1:3]}/pdb{pdb_id}.ent.gz" + ftp.retrbinary(f"RETR {ftp_loc}", fileobj.write) + except ftplib.all_errors as err: + raise IOError(f"Could not retrieve PDB ID {pdb_id}; {err}") from err + finally: + ftp.close() + # Rewind, wrap it in a GzipFile and send it to parse + fileobj.seek(0) + fileobj = io.TextIOWrapper(gzip.GzipFile(fileobj=fileobj, mode='r')) + if saveto is not None: + with closing(genopen(saveto, 'w')) as f: + f.write(fileobj.read()) + fileobj.seek(0) + return PDBFile.parse(fileobj) + + #=================================================== + + _relatere = re.compile(r'RELATED ID: *(\w+) *RELATED DB: *(\w+)', re.I) + + def _add_metadata_fields(self): + self.struct.experimental = '' + self.struct.journal = '' + self.struct.authors = '' + self.struct.keywords = '' + self.struct.doi = '' + self.struct.pmid = '' + self.struct.journal_authors = '' + self.struct.volume = '' + self.struct.title = '' + self.struct.year = None + self.struct.resolution = None + self.struct.related_entries = [] + + @classmethod + def parse(cls, filename, skip_bonds=False): + """ Read a PDB file and return a populated `Structure` class + + Parameters + ---------- + filename : str or file-like + Name of the PDB file to read, or a file-like object that can iterate + over the lines of a PDB. Compressed file names can be specified and + are determined by file-name extension (e.g., file.pdb.gz, + file.pdb.bz2) + skip_bonds : bool, optional + If True, skip trying to assign bonds. This can save substantial time + when parsing large files with non-standard residue names. However, + no bonds are assigned. This is OK if, for instance, the PDB file is + being parsed simply for its coordinates. This may also reduce + element assignment if element information is not present in the PDB + file already. Default is False. + + Metadata + -------- + The PDB parser also adds metadata to the returned Structure object that + may be present in the PDB file + + experimental : ``str`` + EXPDTA record + journal : ``str`` + JRNL record + authors : ``str`` + AUTHOR records + keywords : ``str`` + KEYWDS records + doi : ``str`` + DOI from the JRNL record + pmid : ``str`` + PMID from the JRNL record + journal_authors : ``str`` + Author info from the JRNL record + volume : ``str`` + Volume of the published article from the JRNL record + page : ``str`` + Page of the published article from the JRNL record + title : ``str`` + TITL section of the JRNL record + year : ``int`` + Year that the article was published, from the JRNL record + resolution : ``float`` + The X-RAY resolution in Angstroms, or None if not found + related_entries : ``list of (str, str)`` + List of entries in other databases + + Returns + ------- + structure : :class:`Structure` + The Structure object initialized with all of the information from + the PDB file. No bonds or other topological features are added by + default. + """ + if isinstance(filename, str): + own_handle = True + fileobj = genopen(filename, 'r') + else: + own_handle = False + fileobj = filename + + inst = cls(fileobj) + inst._add_metadata_fields() + + # Support hexadecimal numbering like that printed by VMD + try: + inst._parse_open_file(fileobj) + finally: + if own_handle: + fileobj.close() + + # Assign bonds based on standard templates and simple distances + if not skip_bonds: + inst.struct.assign_bonds() + + inst._postprocess_metadata() + inst.struct.unchange() + + try: + inst.struct.coordinates = inst._coordinates + except ValueError: + raise PDBError('Coordinate shape mismatch. Probably caused by different atom counts in ' + 'some of the PDB models') + + # Postprocess features of the PDB file we couldn't resolve until the end + inst._process_structure_symmetry() + inst._process_link_records() + inst._assign_anisou_to_atoms() + if inst._residue_indices_overflow: + # If we have overflows in our residue numbers, wipe out all insertion codes. CHARMM + # abuses the PDB format and usurps the insertion code (and later fields) to extend the + # residue number. I don't feel bad discarding insertion code information when the PDB + # format is abused like this. It's really never used for overflowing PDBs in my + # experience anyway + for residue in inst.struct.residues: + residue.insertion_code = '' + + return inst.struct + + def _parse_open_file(self, fileobj): + method_dispatch = { + 'REMARK': self._parse_remarks, + 'ATOM': self._parse_atom_record, + 'ANISOU': self._parse_anisou_record, + 'TER': self._parse_ter_record, + 'HETATM': self._parse_atom_record, + 'EXPDTA': self._parse_experimental, + 'AUTHOR': self._parse_author, + 'JRNL': self._parse_journal, + 'KEYWDS': self._parse_keywords, + 'CONECT': self._parse_connect_record, + 'LINK': self._parse_links, + 'CRYST1': self._parse_cryst1, + 'MODEL': self._new_model, + 'ENDMDL': self._end_model, + } + + for line in self.fileobj: + rec = line[:6].strip() + if rec in method_dispatch: + method_dispatch[rec](line) + + def _parse_remarks(self, line): + """ Parse the various remarks, which may contain various metadata """ + if line[6:10] == ' 290': + self._symmetry_lines.append(line) + elif line[6:10] == ' 900': + rematch = self._relatere.match(line[11:]) + if rematch: + self.struct.related_entries.append(rematch.groups()) + elif line[6:10] == ' 2': # Resolution + if (not line[11:22].strip() or self.struct.resolution is not None + or line[23:38] == 'NOT APPLICABLE.'): + return + elif line[11:22] != 'RESOLUTION.': + warnings.warn('Unrecognized RESOLUTION record in PDB file: %s' % line.strip()) + else: + try: + self.struct.resolution = float(line[23:30]) + except ValueError: + warnings.warn('Trouble converting resolution (%s) to float' % line[23:30]) + + def _parse_links(self, line): + self._link_lines.append(line) + + def _parse_keywords(self, line): + self.struct.keywords += '%s,' % line[10:].strip() + + def _parse_ter_record(self, *args): + if self._current_model_number == 1: + self._last_atom.residue.ter = True + + def _parse_experimental(self, line): + if self.struct.experimental: + self.struct.experimental += ' %s' % line[6:].strip() + else: + self.struct.experimental += line[6:].strip() + + def _parse_author(self, line): + if self.struct.authors: + self.struct.authors += ' %s' % line[10:].strip() + else: + self.struct.authors = line[10:].strip() + + def _parse_journal(self, line): + part = line[12:16] + if part == 'AUTH': + self.struct.journal_authors += line[19:].strip() + elif part == 'TITL': + self.struct.title += ' %s' % line[19:].strip() + elif part == 'REF ': + self.struct.journal += ' %s' % line[19:47].strip() + if not line[16:18].strip(): + self.struct.volume = line[51:55].strip() + self.struct.page = line[56:61].strip() + try: + self.struct.year = int(line[62:66]) + except ValueError: + # Shouldn't happen, but don't throw a fit + pass + elif part == 'PMID': + self.struct.pmid = line[19:].strip() + elif part == 'DOI ': + self.struct.doi = line[19:].strip() + + def _parse_cryst1(self, line): + """ Parses unit cell information from the CRYST1 record """ + a = float(line[6:15]) + b = float(line[15:24]) + c = float(line[24:33]) + try: + A = float(line[33:40]) + B = float(line[40:47]) + C = float(line[47:54]) + except (IndexError, ValueError): + A = B = C = 90.0 + self.struct.box = [a, b, c, A, B, C] + self.struct.space_group = line[55:66].strip() + + @staticmethod + def _parse_atom_parts_1(line): + return dict( + number=line[6:11], name=line[12:16].strip(), alternate_location=line[16].strip(), + residue_name=line[17:21].strip(), chain=line[21].strip(), + residue_number=line[22:26].strip(), insertion_code=line[26].strip(), + ) + + @classmethod + def _parse_atom_parts(cls, line): + """ Pulls out atom attributes from the line and packs it into a dict """ + # segment_id is CHARMM-specific + parts = cls._parse_atom_parts_1(line) + parts.update(dict( + x=float(line[30:38]), y=float(line[38:46]), z=float(line[46:54]), + occupancy=line[54:60], bfactor=line[60:66], element=line[76:78], + charge=line[78:80], segment_id=line[72:76].strip(), + )) + + elem = '%-2s' % parts['element'] + # Make sure the space is at the end + elem = elem[1] + ' ' if elem[0] == ' ' else elem + try: + elem = (elem[0].upper() + elem[1].lower()).strip() + atomic_number = AtomicNum[elem] + except KeyError: + elem = element_by_name(parts['name']) + atomic_number = AtomicNum[elem] + parts['atomic_number'] = atomic_number + parts['mass'] = Mass[elem] + parts['bfactor'] = try_convert(parts['bfactor'], float, 0.0) + parts['occupancy'] = try_convert(parts['occupancy'], float, 0.0) + parts['charge'] = try_convert(parts['charge'], float, 0.0) + + return parts + + def _determine_residue_number(self, residue_number, line): + if self._last_atom is not None: + last_residue_number = self._last_atom.residue.number + else: + last_residue_number = 0 + extended_by = self._residue_number_field_extended_by + if self._residue_indices_overflow: + if extended_by: + try: + self._last_residue_number_label = residue_number + line[26:26+extended_by] + residue_number = int(residue_number + line[26:26+extended_by]) + return residue_number + except ValueError: + pass + else: + if residue_number == self._last_residue_number_label: + return last_residue_number + else: + self._last_residue_number_label = residue_number + return last_residue_number + 1 + if last_residue_number >= 9999 and residue_number == '1000' + '0' * extended_by and \ + line[26+extended_by] == '0': + self._residue_indices_overflow = True + self._residue_number_field_extended_by += 1 + self._last_residue_number_label = '1000' + '0' * self._residue_number_field_extended_by + return int(self._last_residue_number_label) + elif last_residue_number == 9999 and residue_number != '9999': + # This is the first time we notice residue indices overflowing + self._residue_indices_overflow = True + self._last_residue_number_label = residue_number + return last_residue_number + 1 + else: + self._last_residue_number_label = residue_number + return int(residue_number) + + def _parse_anisou_record(self, line): + parts = self._parse_atom_parts_1(line) + try: + u11 = int(line[28:35]) + u22 = int(line[35:42]) + u33 = int(line[42:49]) + u12 = int(line[49:56]) + u13 = int(line[56:63]) + u23 = int(line[63:70]) + except ValueError: + warnings.warn('Problem parsing anisotropic factors from ANISOU record from: ' + line, + PDBWarning) + else: + anisou = np.array([u11/1e4, u22/1e4, u33/1e4, u12/1e4, u13/1e4, u23/1e4]) + key = self._make_atom_key_from_parts(parts, all_parts=True) + self._anisou_records[key] = (anisou, line) + + def _determine_atom_number(self, atom_number): + if self._last_atom is not None: + self._atom_indices_overflow = (self._atom_indices_overflow or + self._last_atom.number >= 99999) + if self._atom_indices_overflow and self._last_atom is not None: + return self._last_atom.number + 1 + try: + return int(atom_number) + except ValueError: + return self._last_atom.number + 1 if self._last_atom is not None else 1 + + @classmethod + def _make_atom_key_from_parts(cls, atom_parts, all_parts=False): + """ If all_parts is False, only key from the values that determine alternate locations """ + return cls.AtomLookupKey( + name=atom_parts['name'], + number=atom_parts['number'] if all_parts else None, + residue_name=atom_parts['residue_name'], + residue_number=atom_parts['residue_number'], + chain=atom_parts['chain'], + insertion_code=atom_parts['insertion_code'], + segment_id=atom_parts.get('segment_id', ''), + alternate_location=atom_parts['alternate_location'] if all_parts else None, + ) + + def _parse_atom_record(self, line): + """ Parses an atom record from a PDB file """ + atom_parts = self._parse_atom_parts(line) + residue_number = self._determine_residue_number(atom_parts['residue_number'], line) + atom_number = self._determine_atom_number(atom_parts['number']) + + AtomClass = ExtraPoint if atom_parts['name'] in ('EP', 'LP') else Atom + atom = AtomClass(atomic_number=atom_parts['atomic_number'], name=atom_parts['name'], + charge=atom_parts['charge'], mass=atom_parts['mass'], + occupancy=atom_parts['occupancy'], bfactor=atom_parts['bfactor'], + altloc=atom_parts['alternate_location'], number=atom_number) + atom.xx = atom_parts['x'] + atom.xy = atom_parts['y'] + atom.xz = atom_parts['z'] + attribute_key = self._make_atom_key_from_parts(atom_parts) + all_attribute_key = self._make_atom_key_from_parts(atom_parts, all_parts=True) + current_atom = self._atom_map_from_attributes.get(attribute_key, None) + if (current_atom is not None and atom_parts['alternate_location'] in _ascii_letters_set and + not self._atom_indices_overflow and not self._residue_indices_overflow): + if self._current_model_number == 1: + current_atom.other_locations[atom_parts['alternate_location']] = atom + self._atom_map_to_parent[atom] = current_atom + if atom_number not in self._atom_map_from_atom_number: + self._atom_map_from_atom_number[atom_number] = atom + # altloc atoms should be reachable in the all_attributes map + self._atom_map_from_all_attributes[all_attribute_key] = atom + return + elif not current_atom in self._model1_atoms_in_structure: + # This is if the atom is not in the structure, but in the alternate locations. We + # don't currently store coordinates for those atoms beyond the first frame. Note + # that this should be incredibly rare, since alt-locs are common in static structure + # determination, like X-Ray crystallography. Ensemble methods like NMR won't have + # alternate locations in addition to multiple frames, so this is not expected to be + # an impactful limitation + return + current_atom_index = self._model_atom_counts[-1] + self._model_atom_counts[-1] += 1 + if self._current_model_number == 1: + self._atom_map_from_all_attributes[all_attribute_key] = atom + self._atom_map_from_attributes[attribute_key] = atom + self._atom_map_from_atom_number[atom_number] = atom + self.struct.add_atom( + atom, + atom_parts['residue_name'], + residue_number, + atom_parts['chain'], + atom_parts['insertion_code'], + atom_parts['segment_id'], + ) + self._model1_atoms_in_structure.add(atom) + self._last_atom = atom + else: + try: + atom_from_first_model = self.struct.atoms[current_atom_index] + except IndexError: + raise PDBError('Atom number mismatch between models') + if (atom_from_first_model.residue.name != atom_parts['residue_name'] or + atom_from_first_model.name != atom_parts['name']): + raise PDBError( + f'Atom/residue name mismatch in different models in model {self._current_model_number} [{line.strip()}]!' + ) + if self._current_model_number == 1 or current_atom in self._model1_atoms_in_structure: + self._coordinates[-1].extend([atom.xx, atom.xy, atom.xz]) + + def _new_model(self, line): + if self._current_model_number == 1 and len(self.struct.atoms) == 0: + return # MODEL 1 + if self._model_open: + warnings.warn( + f'{line.strip()} begun before last model ended. Assuming it is ending', + PDBWarning, + ) + self._end_model(line) + self._coordinates.append([]) + self._model_atom_counts.append(0) + self._model_open = True + self._current_model_number += 1 + + def _end_model(self, line): + """ + Ends the current model and validates the processed model is the same size as the models + that came before this one + """ + if not self._model_open: + raise PDBError('No model was begun before ENDMDL was encountered') + self._model_open = False + if len(self._atom_map_from_attributes) == 0: + raise PDBError('No atoms found in model') + if len(self._coordinates[-1]) != 3 * len(self._atom_map_from_attributes): + raise PDBError(f'Coordinate mismatch in model {self._current_model_number}') + + def _parse_connect_record(self, line): + """ + Parses the CONECT records and creates the bond. According to the format spec, the first two + atom indexes are required. The final 3 are optional. + """ + origin_index = try_convert(line[6:11], int) + index_1 = try_convert(line[11:16], int) + index_2 = try_convert(line[16:21], int) + index_3 = try_convert(line[21:26], int) + index_4 = try_convert(line[26:31], int) + if origin_index is None or index_1 is None: + warnings.warn( + f'Bad CONECT record -- not enough atom indexes in line: {line}', PDBWarning + ) + return + origin_atom = self._atom_map_from_atom_number.get(origin_index, None) + atom_1 = self._atom_map_from_atom_number.get(index_1, None) + atom_2 = self._atom_map_from_atom_number.get(index_2, None) + atom_3 = self._atom_map_from_atom_number.get(index_3, None) + atom_4 = self._atom_map_from_atom_number.get(index_4, None) + if origin_atom is None or atom_1 is None: + warnings.warn( + f'CONECT record - could not find atoms {origin_index} and/or {index_1} to connect. Line: {line}', + PDBWarning, + ) + return + origin_atom = self._atom_map_to_parent.get(origin_atom, origin_atom) + for partner in (atom_1, atom_2, atom_3, atom_4): + partner = self._atom_map_to_parent.get(partner, partner) + if partner is None or partner in origin_atom.bond_partners: + continue + self.struct.bonds.append(Bond(origin_atom, partner)) + + def _process_structure_symmetry(self): + if self._symmetry_lines: + data = [] + for line in self._symmetry_lines: + if line.strip().startswith('REMARK 290 SMTRY'): + data.append(line.split()[4:]) + tensor = np.asarray(data, dtype='f8') + self.struct.symmetry = Symmetry(tensor) + + def _process_link_records(self): + for line in self._link_lines: + atom_1_parts = self._parse_atom_parts_1(line) + atom_2_parts = self._parse_atom_parts_1(line[30:]) + + symop1 = line[59:65].strip() + symop2 = line[66:72].strip() + try: + length = float(line[73:78]) + except ValueError: + warnings.warn(f'Malformed LINK line (bad distance): {line}', PDBWarning) + continue + + key1 = self._make_atom_key_from_parts(atom_1_parts) + key2 = self._make_atom_key_from_parts(atom_2_parts) + try: + a1 = self._atom_map_from_attributes[key1] + a2 = self._atom_map_from_attributes[key2] + except KeyError: + warnings.warn(f'Could not find link atoms {key1} and {key2}', PDBWarning) + else: + self.struct.links.append(Link(a1, a2, length, symop1, symop2)) + + def _assign_anisou_to_atoms(self): + """ Assigns the ANISOU tensors to the atoms they belong to """ + for key, (anisou_tensor, line) in self._anisou_records.items(): + try: + self._atom_map_from_all_attributes[key].anisou = anisou_tensor + except KeyError: + warnings.warn( + f'Could not find atom belonging to anisou tensor with key {key}. Line: {line}', + PDBWarning + ) + + def _postprocess_metadata(self): + self.struct.keywords = [s.strip() for s in self.struct.keywords.split(',') if s.strip()] + self.struct.journal = self.struct.journal.strip() + self.struct.title = self.struct.title.strip() + + @staticmethod + def write(struct, dest, renumber=True, coordinates=None, altlocs='all', + write_anisou=False, charmm=False, use_hetatoms=True, + standard_resnames=False, increase_tercount=True, write_links=False): + """ Write a PDB file from a Structure instance + + Parameters + ---------- + struct : :class:`Structure` + The structure from which to write the PDB file + dest : str or file-like + Either a file name or a file-like object containing a `write` + method to which to write the PDB file. If it is a filename that + ends with .gz or .bz2, a compressed version will be written using + either gzip or bzip2, respectively. + renumber : bool, optional, default True + If True, renumber the atoms and residues sequentially as they are + stored in the structure. If False, use the original numbering if + it was assigned previously. + coordinates : array-like of float, optional + If provided, these coordinates will be written to the PDB file + instead of the coordinates stored in the structure. These + coordinates should line up with the atom order in the structure + (not necessarily the order of the "original" PDB file if they + differ) + altlocs : str, optional, default 'all' + Keyword controlling which alternate locations are printed to the + resulting PDB file. Allowable options are: + + - 'all' : print all alternate locations + - 'first' : print only the first alternate locations + - 'occupancy' : print the one with the largest occupancy. If two + conformers have the same occupancy, the first one to occur is + printed + + Input is case-insensitive, and partial strings are permitted as long + as it is a substring of one of the above options that uniquely + identifies the choice. + write_anisou : bool, optional, default False + If True, an ANISOU record is written for every atom that has one. If + False, ANISOU records are not written. + charmm : bool, optional, default False + If True, SEGID will be written in columns 73 to 76 of the PDB file + in the typical CHARMM-style PDB output. This will be omitted for any + atom that does not contain a SEGID identifier. + use_hetatoms: bool, optional, default True + If True, certain atoms will have the HETATM tag instead of ATOM + as per the PDB-standard. + standard_resnames : bool, optional, default False + If True, common aliases for various amino and nucleic acid residues + will be converted into the PDB-standard values. + increase_tercount : bool, optional, default True + If True, the TER atom number field increased by one compared to + atom card preceding it; this conforms to PDB standard. + write_links : bool, optional, default False + If True, any LINK records stored in the Structure will be written to + the LINK records near the top of the PDB file. If this is True, then + renumber *must* be False or a ValueError will be thrown + + Notes + ----- + If multiple coordinate frames are present, these will be written as + separate models (but only the unit cell from the first model will be + written, as the PDB standard dictates that only one set of unit cells + shall be present). + """ + # Determine if we have *any* atom or residue numbers set. If none of + # them are set, force renumbering + no_atom_numbers_assigned = {a.number for a in struct.atoms} == {-1} + no_residue_numbers_assigned = {r.number for r in struct.residues} == {-1} + renumber = renumber or (no_atom_numbers_assigned and no_residue_numbers_assigned) + if renumber and write_links: + raise ValueError('write_links requires renumber=False AND original (not implied) ' + 'numbers to be assigned') + if altlocs.lower() == 'all'[:len(altlocs)]: + altlocs = 'all' + elif altlocs.lower() == 'first'[:len(altlocs)]: + altlocs = 'first' + elif altlocs.lower() == 'occupancy'[:len(altlocs)]: + altlocs = 'occupancy' + else: + raise ValueError("Illegal value of occupancy [%s]; expected 'all', " + "'first', or 'occupancy'" % altlocs) + own_handle = False + if not hasattr(dest, 'write'): + dest = genopen(dest, 'w') + own_handle = True + if charmm: + atomrec = ('ATOM %5d %-4s%1s%-4s%1s%4d%1s %8.3f%8.3f%8.3f%6.2f' + '%6.2f %-4s%2s%-2s\n') + anisourec = 'ANISOU%5d %-4s%1s%-4s%1s%4d%1s %7d%7d%7d%7d%7d%7d %2s%-2s\n' + terrec = 'TER %5d %-4s%1s%4d\n' + reslen = 4 + else: + atomrec = ('ATOM %5d %-4s%1s%-3s %1s%4d%1s %8.3f%8.3f%8.3f%6.2f' + '%6.2f %-4s%2s%-2s\n') + anisourec = 'ANISOU%5d %-4s%1s%-3s %1s%4d%1s %7d%7d%7d%7d%7d%7d %2s%-2s\n' + terrec = ('TER %5d %-3s %1s%4d\n') + reslen = 3 + linkrec = ('LINK %-4s%1s%-3s %1s%4d%1s ' + '%-4s%1s%-3s %1s%4d%1s %6s %6s %5.2f\n') + hetatomrec = atomrec.replace('ATOM ', 'HETATM') if use_hetatoms else atomrec + if struct.box is not None: + dest.write('CRYST1%9.3f%9.3f%9.3f%7.2f%7.2f%7.2f %-11s%4s\n' % ( + struct.box[0], struct.box[1], struct.box[2], struct.box[3], + struct.box[4], struct.box[5], struct.space_group, '')) + if struct.symmetry is not None: + fmt = '%d%4d%10.6f%10.6f%10.6f%15.5f\n' + for index, arr in enumerate(struct.symmetry.data): + arr_list = [1 + index % 3, 1 + index//3] + arr.tolist() + symm_line = "REMARK 290 SMTRY" + fmt % tuple(arr_list) + dest.write(symm_line) + if coordinates is not None: + coords = np.array(coordinates, copy=False, subok=True) + try: + coords = coords.reshape((-1, len(struct.atoms), 3)) + except ValueError: + raise TypeError("Coordinates has unexpected shape") + else: + coords = struct.get_coordinates('all') + if coords is None: + raise ValueError('Cannot write PDB file with no coordinates') + # Create a function to process each atom and return which one we want + # to print, based on our alternate location choice + if altlocs == 'all': + def print_atoms(atom, coords): + return atom, atom.other_locations, coords[atom.idx] + elif altlocs == 'first': + def print_atoms(atom, coords): + return atom, dict(), coords[atom.idx] + elif altlocs == 'occupancy': + def print_atoms(atom, coords): + occ = atom.occupancy + a = atom + for key, item in atom.other_locations.items(): + if item.occupancy > occ: + occ = item.occupancy + a = item + return a, dict(), [a.xx, a.xy, a.xz] + else: + assert False, 'Should not be here' + if standard_resnames: + standardize = lambda x: _standardize_resname(x)[:reslen] + else: + standardize = lambda x: (x[:reslen], _is_hetatm(x)) + nmore = 0 # how many *extra* atoms have been added? + last_number = 0 + if write_links: + for link in struct.links: + rec = ( + _format_atom_name_for_pdb(link.atom1), + link.atom1.altloc, + link.atom1.residue.name, + link.atom1.residue.chain, + link.atom1.residue.number, + link.atom1.residue.insertion_code, + + _format_atom_name_for_pdb(link.atom2), + link.atom2.altloc, + link.atom2.residue.name, + link.atom2.residue.chain, + link.atom2.residue.number, + link.atom2.residue.insertion_code, + + link.symmetry_op1, + link.symmetry_op2, + link.length, + ) + dest.write(linkrec % rec) + for model, coord in enumerate(coords): + if coords.shape[0] > 1: + dest.write('MODEL %5d\n' % (model+1)) + for res in struct.residues: + if renumber: + atoms = res.atoms + else: + atoms = sorted(res.atoms, key=lambda atom: atom.number) + if charmm: + segid = (res.segid or res.chain)[:4] + else: + segid = '' + for atom in atoms: + pa, others, (x, y, z) = print_atoms(atom, coord) + # Figure out the serial numbers we want to print + if renumber: + anum = _number_truncated_to_n_digits(atom.idx + 1 + nmore, 5) + rnum = _number_truncated_to_n_digits(res.idx + 1, 4) + else: + anum = _number_truncated_to_n_digits(pa.number, 5) + rnum = _number_truncated_to_n_digits(res.number, 4) + last_number = anum + # Do any necessary name munging to respect the PDB spec + aname = _format_atom_name_for_pdb(pa) + resname, hetatom = standardize(res.name) + if hetatom: + rec = hetatomrec + else: + rec = atomrec + dest.write(rec % (anum, aname, pa.altloc, resname, + res.chain[:1], rnum, res.insertion_code[:1], + x, y, z, pa.occupancy, pa.bfactor, segid, + Element[pa.atomic_number].upper(), '')) + if write_anisou and pa.anisou is not None: + anisou = [int(ani*1e4) for ani in pa.anisou] + dest.write(anisourec % (anum, aname, pa.altloc, + resname, res.chain[:1], rnum, + res.insertion_code[:1], anisou[0], anisou[1], + anisou[2], anisou[3], anisou[4], anisou[5], + Element[pa.atomic_number].upper(), '')) + for key in sorted(others.keys()): + oatom = others[key] + x, y, z = oatom.xx, oatom.xy, oatom.xz + if renumber: + nmore += 1 + anum = (pa.idx + 1 + nmore) + else: + anum = oatom.number or last_number + 1 + anum = anum - anum // 100000 * 100000 + last_number = anum + aname = _format_atom_name_for_pdb(oatom) + dest.write(rec % (anum, aname, key, resname, + res.chain[:1], rnum, res.insertion_code[:1], + x, y, z, oatom.occupancy, oatom.bfactor, segid, + Element[oatom.atomic_number].upper(), '')) + if write_anisou and oatom.anisou is not None: + anisou = [int(ani*1e4) for ani in oatom.anisou] + el = Element[oatom.atomic_number].upper() + dest.write(anisourec % (anum, aname, + oatom.altloc[:1], resname, res.chain[:1], + rnum, res.insertion_code[:1], anisou[0], + anisou[1], anisou[2], anisou[3], + anisou[4], anisou[5], el, '')) + if res.ter or (len(struct.bonds) > 0 and _needs_ter_card(res)): + if increase_tercount: + dest.write(terrec % (anum+1, resname, res.chain, rnum)) + if renumber: + nmore += 1 + else: + last_number += 1 + else: + dest.write(terrec % (anum, resname, res.chain, rnum)) + if coords.shape[0] > 1: + dest.write('ENDMDL\n') + + dest.write("%-80s\n" % "END") + if own_handle: + dest.close() + +#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +class CIFFile(metaclass=FileFormatType): + """ Standard PDBx/mmCIF file format parser and writer """ + #=================================================== + + @staticmethod + def id_format(filename): + """ Identifies the file type as a PDBx/mmCIF file + + Parameters + ---------- + filename : str + Name of the file to check format for + + Returns + ------- + is_fmt : bool + True if it is a PDBx/mmCIF file + """ + f = genopen(filename) + try: + for line in f: + if line.startswith('#'): continue + if line[:5] == 'data_' and len(line.split()) == 1: + return True + else: + return False + return False + finally: + f.close() + + #=================================================== + + @staticmethod + def download(pdb_id, timeout=10, saveto=None): + """ + Goes to the wwPDB website and downloads the requested PDBx/mmCIF, + loading it as a :class:`Structure` instance + + Parameters + ---------- + pdb_id : str + The 4-letter PDB ID to try and download from the RCSB PDB database + timeout : float, optional + The number of seconds to wait before raising a timeout error. + Default is 10 seconds + saveto : str, optional + If provided, this will be treated as a file name to which the PDB + file will be saved. If None (default), no CIF file will be written. + This will be a verbatim copy of the downloaded CIF file, unlike the + somewhat-stripped version you would get by using + :meth:`Structure.write_cif ` + + Returns + ------- + struct : :class:`Structure ` + Structure instance populated by the requested PDBx/mmCIF + + Raises + ------ + socket.timeout if the connection times out while trying to contact the + FTP server + + IOError if there is a problem retrieving the requested PDB + + ImportError if the gzip module is not available + + TypeError if pdb_id is not a 4-character string + """ + if not isinstance(pdb_id, str) or len(pdb_id) != 4: + raise ValueError('pdb_id must be the 4-letter PDB code') + + pdb_id = pdb_id.lower() + ftp = ftplib.FTP('ftp.wwpdb.org', timeout=timeout) + ftp.login() + fileobj = io.BytesIO() + try: + ftp.retrbinary('RETR /pub/pdb/data/structures/divided/mmCIF/' + '%s/%s.cif.gz' % (pdb_id[1:3], pdb_id), + fileobj.write) + except ftplib.all_errors as err: + raise IOError('Could not retrieve PDB ID %s; %s' % (pdb_id, err)) + finally: + ftp.close() + fileobj.seek(0) + fileobj = io.TextIOWrapper(gzip.GzipFile(fileobj=fileobj, mode='r')) + if saveto is not None: + with closing(genopen(saveto, 'w')) as f: + f.write(fileobj.read()) + fileobj.seek(0) + return CIFFile.parse(fileobj) + + #=================================================== + + @staticmethod + def parse(filename, skip_bonds=False): + """ + Read a PDBx or mmCIF file and return a populated `Structure` class + + Parameters + ---------- + filename : ``str or file-like`` + Name of PDB file to read, or a file-like object that can iterate + over the lines of a PDB. Compressed file names can be specified and + are determined by file-name extension (e.g., file.pdb.gz, + file.pdb.bz2) + skip_bonds : bool, optional + If True, skip trying to assign bonds. This can save substantial time + when parsing large files with non-standard residue names. However, + no bonds are assigned. This is OK if, for instance, the CIF file is + being parsed simply for its coordinates. Default is False. + + Metadata + -------- + The PDB parser also adds metadata to the returned Structure object that + may be present in the PDB file + + experimental : ``str`` + EXPDTA record + journal : ``str`` + JRNL record + authors : ``str`` + AUTHOR records + keywords : ``str`` + KEYWDS records + doi : ``str`` + DOI from the JRNL record + pmid : ``str`` + PMID from the JRNL record + journal_authors : ``str`` + Author info from the JRNL record + volume : ``str`` + Volume of the published article from the JRNL record + page : ``str`` + Page of the published article from the JRNL record + title : ``str`` + TITL section of the JRNL record + year : ``str`` + Year that the article was published, from the JRNL record + resolution : ``float`` + The X-RAY resolution in Angstroms, or None if not found + related_entries : ``list of (str, str)`` + List of entries in other databases + + Returns + ------- + structure1 [, structure2 [, structure3 [, ...] ] ] + + structure# : :class:`Structure` + The Structure object initialized with all of the information from + the PDBx/mmCIF file. No bonds or other topological features are + added by default. If multiple structures are defined in the CIF + file, multiple Structure instances will be returned as a tuple. + + Raises + ------ + ValueError if the file severely violates the PDB format specification. + If this occurs, check the formatting on each line and make sure it + matches the others. + """ + if isinstance(filename, str): + own_handle = True + fileobj = genopen(filename, 'r') + else: + own_handle = False + fileobj = filename + + try: + cifobj = PdbxReader(fileobj) + data = [] + cifobj.read(data) + finally: + if own_handle: fileobj.close() + + structures = [] + for cont in data: + struct = Structure() + structures.append(struct) + # Add metadata fields + struct.experimental = struct.journal = struct.authors = '' + struct.keywords = struct.doi = struct.pmid = '' + struct.journal_authors = struct.volume = struct.title = '' + struct.year = struct.resolution = None + struct.related_entries = [] + + # Now we have the data. First get the metadata if it exists + exptl = cont.getObj('exptl') + if exptl is not None: + struct.experimental = exptl.getValue('method') + auth = cont.getObj('audit_author') + if auth is not None: + nameidx = auth.getAttributeIndex('name') + if nameidx != -1: + struct.authors = ', '.join([t[nameidx] for t in + auth.getRowList()]) + reflns = cont.getObj('reflns') + if reflns is not None: + res = reflns.getValue('d_resolution_high') + if res != '?': + try: + struct.resolution = float(res) + except ValueError: + warnings.warn('Could not convert resolution (%s) to float' % res) + cite = cont.getObj('citation_author') + if cite is not None: + nameidx = cite.getAttributeIndex('name') + if nameidx != -1: + journal_authors = [] + for i in range(cite.getRowCount()): + a = cite.getRow(i)[nameidx] + if a not in journal_authors: + journal_authors.append(a) + struct.journal_authors = ', '.join(journal_authors) + cite = cont.getObj('citation') + if cite is not None: + doiid = cite.getAttributeIndex('pdbx_database_id_DOI') + pmiid = cite.getAttributeIndex('pdbx_database_id_PubMed') + titlid = cite.getAttributeIndex('title') + yearid = cite.getAttributeIndex('year') + pageid = cite.getAttributeIndex('page_first') + jrnlid = cite.getAttributeIndex('journal_abbrev') + volid = cite.getAttributeIndex('journal_volume') + rows = cite.getRowList() + if doiid != -1: + struct.doi = ', '.join([row[doiid] for row in rows + if row[doiid] != '?']) + if pmiid != -1: + struct.pmid = ', '.join([row[pmiid] for row in rows + if row[pmiid] != '?']) + if titlid != -1: + struct.title = '; '.join([row[titlid] for row in rows]) + if yearid != -1: + struct.year = ', '.join([row[yearid] for row in rows]) + if pageid != -1: + struct.page = ', '.join([row[pageid] for row in rows]) + if jrnlid != -1: + struct.journal = '; '.join([row[jrnlid] for row in rows]) + if volid != -1: + struct.volume = ', '.join([row[volid] for row in rows]) + keywds = cont.getObj('struct_keywords') + if keywds is not None: + textid = keywds.getAttributeIndex('text') + if textid != -1: + rows = keywds.getRowList() + struct.keywords = ', '.join([row[textid] for row in rows]) + struct.keywords = [key.strip() for key in + struct.keywords.split(',') if key.strip()] + dbase = cont.getObj('pdbx_database_related') + if dbase is not None: + dbid = dbase.getAttributeIndex('db_id') + nameid = dbase.getAttributeIndex('db_name') + if dbid != -1 and nameid != -1: + rows = dbase.getRowList() + struct.related_entries = [(r[dbid],r[nameid]) for r in rows] + # Now go through all of the atoms. Any items that do *not* exist are + # given an index of -1. So we append an empty string on the end of + # each row so that the default value for any un-specified value is + # the empty string. This avoids needing any conditionals inside the + # loop + atoms = cont.getObj('atom_site') + atnumid = atoms.getAttributeIndex('id') + elemid = atoms.getAttributeIndex('type_symbol') + atnameid = atoms.getAttributeIndex('auth_atom_id') + altlocid = atoms.getAttributeIndex('label_alt_id') + resnameid = atoms.getAttributeIndex('auth_comp_id') + chainid = atoms.getAttributeIndex('auth_asym_id') + resnumid = atoms.getAttributeIndex('auth_seq_id') + inscodeid = atoms.getAttributeIndex('pdbx_PDB_ins_code') + xid = atoms.getAttributeIndex('Cartn_x') + yid = atoms.getAttributeIndex('Cartn_y') + zid = atoms.getAttributeIndex('Cartn_z') + occupid = atoms.getAttributeIndex('occupancy') + bfactorid = atoms.getAttributeIndex('B_iso_or_equiv') + modelid = atoms.getAttributeIndex('pdbx_PDB_model_num') + origmodel = None + lastmodel = None + all_coords = [] + xyz = [] + atommap = dict() + last_atom = Atom() + for i in range(atoms.getRowCount()): + row = atoms.getRow(i) + [''] + atnum = int(row[atnumid]) + elem = row[elemid] + atname = row[atnameid] + altloc = row[altlocid] + if altloc == '.': altloc = '' + resname = row[resnameid] + chain = row[chainid] + resnum = int(row[resnumid]) + inscode = row[inscodeid] + if inscode in '?.': inscode = '' + model = int(row[modelid]) + if origmodel is None: + origmodel = lastmodel = model + x, y, z = float(row[xid]), float(row[yid]), float(row[zid]) + occup = float(row[occupid]) + bfactor = float(row[bfactorid]) + # Try to figure out the element + elem = '%-2s' % elem # Make sure we have at least 2 characters + if elem[0] == ' ': elem = elem[1] + ' ' + try: + atsym = (elem[0] + elem[1].lower()).strip() + atomic_number = AtomicNum[atsym] + mass = Mass[atsym] + except KeyError: + # Now try based on the atom name... but don't try too hard + # (e.g., don't try to differentiate b/w Ca and C) + try: + atomic_number = AtomicNum[atname.strip()[0].upper()] + mass = Mass[atname.strip()[0].upper()] + except KeyError: + try: + sym = atname.strip()[:2] + sym = '%s%s' % (sym[0].upper(), sym[1].lower()) + atomic_number = AtomicNum[sym] + mass = Mass[sym] + except KeyError: + atomic_number = 0 # give up + mass = 0.0 + if atname.startswith('EP') or atname.startswith('LP'): + atom = ExtraPoint(atomic_number=atomic_number, name=atname, + mass=mass, occupancy=occup, bfactor=bfactor, + altloc=altloc, number=atnum) + else: + atom = Atom(atomic_number=atomic_number, name=atname, + mass=mass, occupancy=occup, bfactor=bfactor, + altloc=altloc, number=atnum) + atom.xx, atom.xy, atom.xz = x, y, z + if (_compare_atoms(last_atom, atom, resname, resnum, + chain, '', inscode) + and altloc): + atom.residue = last_atom.residue + last_atom.other_locations[altloc] = atom + else: + if model == origmodel: + # Only add the atoms once + struct.add_atom(atom, resname, resnum, chain, inscode) + last_atom = atom + if model == lastmodel: + xyz.extend([x, y, z]) + else: + if all_coords and len(xyz) != len(all_coords[-1]): + raise ValueError('All frames must have same number ' + 'of atoms') + all_coords.append(xyz) + xyz = [x, y, z] + lastmodel = model + # Keep a mapping in case we need to go back and add attributes, + # like anisotropic b-factors + if model == origmodel: + key = (resnum,resname,inscode,chain,atnum,altloc,atname) + atommap[key] = atom + # Check for unit cell parameters + cell = cont.getObj('cell') + if cell is not None: + aid = cell.getAttributeIndex('length_a') + bid = cell.getAttributeIndex('length_b') + cid = cell.getAttributeIndex('length_c') + alphaid = cell.getAttributeIndex('angle_alpha') + betaid = cell.getAttributeIndex('angle_beta') + gammaid = cell.getAttributeIndex('angle_gamma') + row = cell.getRow(0) + struct.box = np.array( + [float(row[aid]), float(row[bid]), float(row[cid]), + float(row[alphaid]), float(row[betaid]), + float(row[gammaid])] + ) + symmetry = cont.getObj('symmetry') + if symmetry is not None: + spaceid = symmetry.getAttributeIndex('space_group_name_H-M') + row = symmetry.getRow(0) + if spaceid != -1: + struct.space_group = row[spaceid] + # Check for anisotropic B-factors + anisou = cont.getObj('atom_site_anisotrop') + if anisou is not None: + atnumid = anisou.getAttributeIndex('id') + atnameid = anisou.getAttributeIndex('pdbx_auth_atom_id') + altlocid = anisou.getAttributeIndex('pdbx_label_alt_id') + resnameid = anisou.getAttributeIndex('pdbx_auth_comp_id') + chainid = anisou.getAttributeIndex('pdbx_auth_asym_id') + resnumid = anisou.getAttributeIndex('pdbx_auth_seq_id') + inscodeid = anisou.getAttributeIndex('pdbx_PDB_ins_code') + u11id = anisou.getAttributeIndex('U[1][1]') + u22id = anisou.getAttributeIndex('U[2][2]') + u33id = anisou.getAttributeIndex('U[3][3]') + u12id = anisou.getAttributeIndex('U[1][2]') + u13id = anisou.getAttributeIndex('U[1][3]') + u23id = anisou.getAttributeIndex('U[2][3]') + if -1 in (atnumid, atnameid, altlocid, resnameid, chainid, + resnumid, u11id, u22id, u33id, u12id, u13id, u23id): + warnings.warn('Incomplete anisotropic B-factor CIF ' + 'section. Skipping', PDBWarning) + else: + try: + for i in range(anisou.getRowCount()): + row = anisou.getRow(i) + [''] + atnum = int(row[atnumid]) + atname = row[atnameid] + altloc = row[altlocid] + resname = row[resnameid] + chain = row[chainid] + resnum = int(row[resnumid]) + inscode = row[inscodeid] + u11 = float(row[u11id]) + u22 = float(row[u22id]) + u33 = float(row[u33id]) + u12 = float(row[u12id]) + u13 = float(row[u13id]) + u23 = float(row[u23id]) + if altloc == '.': altloc = '' + if inscode in '?.': inscode = '' + key = (resnum, resname, inscode, chain, atnum, + altloc, atname) + atommap[key].anisou = np.array( + [u11, u22, u33, u12, u13, u23] + ) + except (ValueError, KeyError): + # If at least one went wrong, set them all to None + for key, atom in atommap.items(): + atom.anisou = None + warnings.warn('Problem processing anisotropic ' + 'B-factors. Skipping', PDBWarning) + if xyz: + if len(xyz) != len(struct.atoms) * 3: + raise ValueError('Corrupt CIF; all models must have the ' + 'same atoms') + all_coords.append(xyz) + if all_coords: + struct._coordinates = np.array(all_coords).reshape( + (-1, len(struct.atoms), 3)) + + # Make sure we assign bonds for all of the structures we parsed + if not skip_bonds: + for struct in structures: + struct.assign_bonds() + # Build the return value + if len(structures) == 1: + return structures[0] + return tuple(structures) + + #=================================================== + + @staticmethod + def write(struct, dest, renumber=True, coordinates=None, + altlocs='all', write_anisou=False, standard_resnames=False): + """ + Write a PDB file from the current Structure instance + + Parameters + ---------- + struct : :class:`Structure` + The structure from which to write the PDBx/mmCIF file + dest : ``str or file-like`` + Either a file name or a file-like object containing a `write` + method to which to write the PDB file. If it is a filename that + ends with .gz or .bz2, a compressed version will be written using + either gzip or bzip2, respectively. + renumber : ``bool`` + If True, renumber the atoms and residues sequentially as they are + stored in the structure. If False, use the original numbering if + it was assigned previously + coordinates : ``array-like of float`` + If provided, these coordinates will be written to the PDB file + instead of the coordinates stored in the structure. These + coordinates should line up with the atom order in the structure + (not necessarily the order of the "original" PDB file if they + differ) + altlocs : ``str`` + Keyword controlling which alternate locations are printed to the + resulting PDB file. Allowable options are: + + - 'all' : (default) print all alternate locations + - 'first' : print only the first alternate locations + - 'occupancy' : print the one with the largest occupancy. If two + conformers have the same occupancy, the first one to occur is + printed + + Input is case-insensitive, and partial strings are permitted as long + as it is a substring of one of the above options that uniquely + identifies the choice. + write_anisou : ``bool`` + If True, an ANISOU record is written for every atom that has one. If + False, ANISOU records are not written + standard_resnames : bool, optional + If True, common aliases for various amino and nucleic acid residues + will be converted into the PDB-standard values. Default is False + + Notes + ----- + If multiple coordinate frames are present, these will be written as + separate models (but only the unit cell from the first model will be + written, as the PDBx standard dictates that only one set of unit cells + shall be present). + """ + if altlocs.lower() == 'all'[:len(altlocs)]: + altlocs = 'all' + elif altlocs.lower() == 'first'[:len(altlocs)]: + altlocs = 'first' + elif altlocs.lower() == 'occupancy'[:len(altlocs)]: + altlocs = 'occupancy' + else: + raise ValueError("Illegal value of occupancy [%s]; expected 'all', " + "'first', or 'occupancy'" % altlocs) + own_handle = False + if not hasattr(dest, 'write'): + dest = genopen(dest, 'w') + own_handle = True + # Make the main container + cont = containers.DataContainer('cell') + # Add cell info if applicable + if struct.box is not None: + cell = containers.DataCategory('cell') + cell.appendAttribute('length_a') + cell.appendAttribute('length_b') + cell.appendAttribute('length_c') + cell.appendAttribute('angle_alpha') + cell.appendAttribute('angle_beta') + cell.appendAttribute('angle_gamma') + cell.append(struct.box[:]) + cont.append(cell) + # symmetry + sym = containers.DataCategory('symmetry') + sym.appendAttribute('space_group_name_H-M') + sym.append([struct.space_group]) + cont.append(sym) + if coordinates is not None: + coords = np.array(coordinates, copy=False, subok=True) + try: + coords = coords.reshape((-1, len(struct.atoms), 3)) + except ValueError: + raise TypeError("Coordinates has unexpected shape") + else: + coords = struct.get_coordinates('all') + if coords is None: + raise ValueError('Cannot write CIF file with no coordinates') + # Create a function to process each atom and return which one we want + # to print, based on our alternate location choice + if altlocs == 'all': + def print_atoms(atom, coords): + return atom, atom.other_locations, coords[atom.idx] + elif altlocs == 'first': + def print_atoms(atom, coords): + return atom, dict(), coords[atom.idx] + elif altlocs == 'occupancy': + def print_atoms(atom, coords): + occ = atom.occupancy + a = atom + for key, item in atom.other_locations.items(): + if item.occupancy > occ: + occ = item.occupancy + a = item + return a, dict(), [a.xx, a.xy, a.xz] + else: + assert False, 'Should not be here' + if standard_resnames: + standardize = lambda x: _standardize_resname(x) + else: + standardize = lambda x: (x, _is_hetatm(x)) + # Now add the atom section. Include all names that the CIF standard + # usually includes, but put '?' in sections that contain data we don't + # store in the Structure, Residue, or Atom classes + cifatoms = containers.DataCategory('atom_site') + cont.append(cifatoms) + cifatoms.setAttributeNameList( + ['group_PDB', 'id', 'type_symbol', 'label_atom_id', + 'label_alt_id', 'label_comp_id', 'label_asym_id', + 'label_entity_id', 'label_seq_id', 'pdbx_PDB_ins_code', + 'Cartn_x', 'Cartn_y', 'Cartn_z', 'occupancy', 'B_iso_or_equiv', + 'Cartn_x_esd', 'Cartn_y_esd', 'Cartn_z_esd', 'occupancy_esd', + 'B_iso_or_equiv_esd', 'pdbx_formal_charge', 'auth_seq_id', + 'auth_comp_id', 'auth_asym_id', 'auth_atom_id', + 'pdbx_PDB_model_num'] + ) + write_anisou = write_anisou and any(atom.anisou is not None + for atom in struct.atoms) + if write_anisou: + cifanisou = containers.DataCategory('atom_site_anisotrop') + cont.append(cifanisou) + cifanisou.setAttributeNameList( + ['id', 'type_symbol', 'pdbx_label_atom_id', + 'pdbx_label_alt_id', 'pdbx_label_comp_id', + 'pdbx_label_asym_id', 'pdbx_label_seq_id', 'U[1][1]', + 'U[2][2]', 'U[3][3]', 'U[1][2]', 'U[1][3]', 'U[2][3]', + 'U[1][1]_esd', 'U[2][2]_esd', 'U[3][3]_esd', 'U[1][2]_esd', + 'U[1][3]_esd', 'U[2][3]_esd', 'pdbx_auth_seq_id', + 'pdbx_auth_comp_id', 'pdbx_auth_asym_id', + 'pdbx_auth_atom_id'] + ) + nmore = 0 # how many *extra* atoms have been added? + last_number = 0 + last_rnumber = 0 + for model, coord in enumerate(coords): + for res in struct.residues: + if renumber: + atoms = res.atoms + else: + atoms = sorted(res.atoms, key=lambda atom: atom.number) + resname, hetatom = standardize(res.name) + if hetatom: + atomrec = 'HETATM' + else: + atomrec = 'ATOM ' + for atom in atoms: + pa, others, (x, y, z) = print_atoms(atom, coord) + # Figure out the serial numbers we want to print + if renumber: + anum = (atom.idx + 1 + nmore) + rnum = (res.idx + 1) + else: + anum = (pa.number or last_number + 1) + rnum = (atom.residue.number or last_rnumber + 1) + last_number = anum + last_rnumber = rnum + cifatoms.append( + [atomrec, anum, Element[pa.atomic_number].upper(), + pa.name, pa.altloc, resname, res.chain, '?', rnum, + res.insertion_code, x, y, z, pa.occupancy, + pa.bfactor, '?', '?', '?', '?', '?', '', rnum, + resname, res.chain, pa.name, str(model+1)] + ) + if write_anisou and pa.anisou is not None: + cifanisou.append( + [anum, Element[pa.atomic_number].upper(), + pa.name, pa.altloc, resname, res.chain, rnum, + pa.anisou[0], pa.anisou[1], pa.anisou[2], + pa.anisou[3], pa.anisou[4], pa.anisou[5], '?', + '?', '?', '?', '?', '?', rnum, resname, + res.chain, pa.name] + ) + for key in sorted(others.keys()): + oatom = others[key] + x, y, z = oatom.xx, oatom.xy, oatom.xz + if renumber: + nmore += 1 + anum = (pa.idx + 1 + nmore) + else: + anum = oatom.number or last_number + 1 + last_number = anum + el = Element[oatom.atomic_number].upper() + cifatoms.append( + [atomrec, anum, el, oatom.name, oatom.altloc, + resname, res.chain, '?', rnum, + res.insertion_code, x, y, z, oatom.occupancy, + oatom.bfactor, '?', '?', '?', '?', '?', '', + rnum, resname, res.chain, oatom.name, '1'] + ) + if write_anisou and oatom.anisou is not None: + cifanisou.append( + [anum, Element[oatom.atomic_number].upper(), + oatom.name, oatom.altloc, resname, + res.chain, rnum, oatom.anisou[0], + oatom.anisou[1], oatom.anisou[2], + oatom.anisou[3], oatom.anisou[4], + oatom.anisou[5], '?', '?', '?', '?', '?', + '?', rnum, resname, res.chain, oatom.name] + ) + # Now write the PDBx file + writer = PdbxWriter(dest) + writer.write([cont]) + if own_handle: + dest.close() + +#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +def _find_atom_index(struct, idx): + """ + Returns the atom with the given index in the structure. This is required + because atom indices may not start from 1 and may contain gaps in a PDB + file. This tries to find atoms quickly, assuming that indices *do* start + from 1 and have no gaps. It then looks up or down, depending on whether we + hit an index too high or too low. So it *assumes* that the sequence is + monotonically increasing. If the atom can't be found, None is returned + """ + idx0 = min(max(idx - 1, 0), len(struct.atoms)-1) + if struct[idx0].number == idx: + return struct[idx0] + if struct[idx0].number < idx: + idx0 += 1 + while idx0 < len(struct.atoms): + if struct[idx0].number == idx: + return struct[idx0] + idx0 += 1 + return None # not found + else: + idx0 -= 1 + while idx0 > 0: + if struct[idx0].number == idx: + return struct[idx0] + idx0 -= 1 + return None # not found + +def _needs_ter_card(res): + """ Determines if a TER card is needed by seeing if the residue is a + polymeric residue that is *not* bonded to the next residue + """ + # First see if it's in the list of standard biomolecular residues. If so, + # and it has no tail, no TER is needed + std_resname = _standardize_resname(res.name)[0] + if std_resname in StandardBiomolecularResidues: + is_std_res = True + if StandardBiomolecularResidues[std_resname].tail is None: + return False + else: + is_std_res = False + my_res_idx = res.idx + residxs = set() + for atom in res.atoms: + for bond in atom.bonds: + residxs |= {bond.atom1.residue.idx, bond.atom2.residue.idx} + if my_res_idx + 1 in residxs: + return False # It's connected to next residue + elif is_std_res: + return True + else: + # Heuristic -- add a TER if it's bonded to the previous residue, which + # indicates it's polymeric. Otherwise don't. + return my_res_idx - 1 in residxs + +def _format_atom_name_for_pdb(atom): + if len(atom.name) < 4 and len(Element[atom.atomic_number]) != 2: + return ' %-3s' % atom.name + return atom.name[:4] + +def try_convert(value, cast_type, default=None): + try: + return cast_type(value) + except ValueError: + return default diff --git a/pmx/workflow/parmed/formats/pdbx/PdbxContainers.py b/pmx/workflow/parmed/formats/pdbx/PdbxContainers.py new file mode 100644 index 00000000..97b5ebdb --- /dev/null +++ b/pmx/workflow/parmed/formats/pdbx/PdbxContainers.py @@ -0,0 +1,806 @@ +## +# +# File: PdbxContainers.py +# Original: 02-Feb-2009 jdw +# +# Update: +# 23-Mar-2011 jdw Added method to rename attributes in category containers. +# 05-Apr-2011 jdw Change cif writer to select double quoting as preferred +# quoting style where possible. +# 16-Jan-2012 jdw Create base class for DataCategory class +# 22-Mar-2012 jdw when append attributes to existing categories update +# existing rows with placeholder null values. +# 2-Sep-2012 jdw add option to avoid embedded quoting that might +# confuse simple parsers. +# 28-Jun-2013 jdw export remove method +# 29-Jun-2013 jdw export remove row method +## +""" + +A collection of container classes supporting the PDBx/mmCIF storage model. + +A base container class is defined which supports common features of +data and definition containers. PDBx data files are organized in +sections called data blocks which are mapped to data containers. +PDBx dictionaries contain definition sections and data sections +which are mapped to definition and data containes respectively. + +Data in both PDBx data files and dictionaries are organized in +data categories. In the PDBx syntax individual items or data +identified by labels of the form '_categoryName.attributeName'. +The terms category and attribute in PDBx jargon are analogous +table and column in relational data model, or class and attribute +in an object oriented data model. + +The DataCategory class provides base storage container for instance +data and definition meta data. + +""" + +__docformat__ = "restructuredtext en" +__author__ = "John Westbrook" +__email__ = "jwest@rcsb.rutgers.edu" +__license__ = "Creative Commons Attribution 3.0 Unported" +__version__ = "V0.01" + +import re,sys,traceback + +class CifName(object): + ''' Class of utilities for CIF-style data names - + ''' + def __init__(self): + pass + + @staticmethod + def categoryPart(name): + tname="" + if name.startswith("_"): + tname=name[1:] + else: + tname=name + + i = tname.find(".") + if i == -1: + return tname + else: + return tname[:i] + + @staticmethod + def attributePart(name): + i = name.find(".") + if i == -1: + return None + else: + return name[i+1:] + +class ContainerBase(object): + ''' Container base class for data and definition objects. + ''' + def __init__(self,name): + # The enclosing scope of the data container (e.g. data_/save_) + self.__name = name + # List of category names within this container - + self.__objNameList=[] + # dictionary of DataCategory objects keyed by category name. + self.__objCatalog={} + self.__type=None + + def getType(self): + return self.__type + + def setType(self,type): + self.__type=type + + def getName(self): + return self.__name + + def setName(self,name): + self.__name=name + + def exists(self,name): + return name in self.__objCatalog + + def getObj(self,name): + if name in self.__objCatalog: + return self.__objCatalog[name] + else: + return None + + def getObjNameList(self): + return self.__objNameList + + def append(self,obj): + """ Add the input object to the current object catalog. An existing object + of the same name will be overwritten. + """ + if obj.getName() is not None: + if not obj.getName() in self.__objCatalog: + # self.__objNameList is keeping track of object order here -- + self.__objNameList.append(obj.getName()) + self.__objCatalog[obj.getName()]=obj + + def replace(self,obj): + """ Replace an existing object with the input object + """ + if obj.getName() is not None and obj.getName() in self.__objCatalog: + self.__objCatalog[obj.getName()]=obj + + + def printIt(self,fh=sys.stdout,type="brief"): + fh.write("+ %s container: %30s contains %4d categories\n" % + (self.getType(),self.getName(),len(self.__objNameList))) + for nm in self.__objNameList: + fh.write("--------------------------------------------\n") + fh.write("Data category: %s\n" % nm) + if type == 'brief': + self.__objCatalog[nm].printIt(fh) + else: + self.__objCatalog[nm].dumpIt(fh) + + + def rename(self,curName,newName): + """ Change the name of an object in place - + """ + try: + i=self.__objNameList.index(curName) + self.__objNameList[i]=newName + self.__objCatalog[newName]=self.__objCatalog[curName] + self.__objCatalog[newName].setName(newName) + return True + except: + return False + + def remove(self,curName): + """ Revmove object by name. Return True on success or False otherwise. + """ + try: + if curName in self.__objCatalog: + del self.__objCatalog[curName] + i=self.__objNameList.index(curName) + del self.__objNameList[i] + return True + else: + return False + except: + pass + + return False + +class DefinitionContainer(ContainerBase): + def __init__(self,name): + super(DefinitionContainer,self).__init__(name) + self.setType('definition') + + def isCategory(self): + if self.exists('category'): + return True + return False + + def isAttribute(self): + if self.exists('item'): + return True + return False + + + def printIt(self,fh=sys.stdout,type="brief"): + fh.write("Definition container: %30s contains %4d categories\n" % + (self.getName(),len(self.getObjNameList()))) + if self.isCategory(): + fh.write("Definition type: category\n") + elif self.isAttribute(): + fh.write("Definition type: item\n") + else: + fh.write("Definition type: undefined\n") + + for nm in self.getObjNameList(): + fh.write("--------------------------------------------\n") + fh.write("Definition category: %s\n" % nm) + if type == 'brief': + self.getObj(nm).printIt(fh) + else: + self.getObj(nm).dumpId(fh) + +class DataContainer(ContainerBase): + ''' Container class for DataCategory objects. + ''' + def __init__(self,name): + super(DataContainer,self).__init__(name) + self.setType('data') + self.__globalFlag=False + + def invokeDataBlockMethod(self,type,method,db): + self.__currentRow = 1 + exec(method.getInline()) + + def setGlobal(self): + self.__globalFlag=True + + def getGlobal(self): + return self.__globalFlag + +class DataCategoryBase(object): + """ Base object definition for a data category - + """ + def __init__(self,name,attributeNameList=None,rowList=None): + self._name = name + # + if rowList is not None: + self._rowList=rowList + else: + self._rowList=[] + + if attributeNameList is not None: + self._attributeNameList=attributeNameList + else: + self._attributeNameList=[] + # + # Derived class data - + # + self._catalog={} + self._numAttributes=0 + # + self.__setup() + + def __setup(self): + self._numAttributes = len(self._attributeNameList) + self._catalog={} + for attributeName in self._attributeNameList: + attributeNameLC = attributeName.lower() + self._catalog[attributeNameLC] = attributeName + # + def setRowList(self,rowList): + self._rowList=rowList + + def setAttributeNameList(self,attributeNameList): + self._attributeNameList=attributeNameList + self.__setup() + + def setName(self,name): + self._name=name + + def get(self): + return (self._name,self._attributeNameList,self._rowList) + +class DataCategory(DataCategoryBase): + """ Methods for creating, accessing, and formatting PDBx cif data categories. + """ + def __init__(self,name,attributeNameList=None,rowList=None): + super(DataCategory,self).__init__(name,attributeNameList,rowList) + # + self.__lfh = sys.stdout + + self.__currentRowIndex=0 + self.__currentAttribute=None + # + self.__avoidEmbeddedQuoting=False + # + # -------------------------------------------------------------------- + # any whitespace + self.__wsRe=re.compile(r"\s") + self.__wsAndQuotesRe=re.compile(r"[\s'\"]") + # any newline or carriage control + self.__nlRe=re.compile(r"[\n\r]") + # + # single quote + self.__sqRe=re.compile(r"[']") + # + self.__sqWsRe=re.compile(r"('\s)|(\s')") + + # double quote + self.__dqRe=re.compile(r'["]') + self.__dqWsRe=re.compile(r'("\s)|(\s")') + # + self.__intRe=re.compile(r'^[0-9]+$') + self.__floatRe=re.compile(r'^-?(([0-9]+)[.]?|([0-9]*[.][0-9]+))([(][0-9]+[)])?([eE][+-]?[0-9]+)?$') + # + self.__dataTypeList=['DT_NULL_VALUE','DT_INTEGER','DT_FLOAT','DT_UNQUOTED_STRING','DT_ITEM_NAME', + 'DT_DOUBLE_QUOTED_STRING','DT_SINGLE_QUOTED_STRING','DT_MULTI_LINE_STRING'] + self.__formatTypeList=['FT_NULL_VALUE','FT_NUMBER','FT_NUMBER','FT_UNQUOTED_STRING', + 'FT_QUOTED_STRING','FT_QUOTED_STRING','FT_QUOTED_STRING','FT_MULTI_LINE_STRING'] + # + + + def __getitem__(self, x): + """ Implements list-type functionality - + Implements op[x] for some special cases - + x=integer - returns the row in category (normal list behavior) + x=string - returns the value of attribute 'x' in first row. + """ + if isinstance(x, int): + #return self._rowList.__getitem__(x) + return self._rowList[x] + + elif isinstance(x, str): + try: + #return self._rowList[0][x] + ii=self.getAttributeIndex(x) + return self._rowList[0][ii] + except (IndexError, KeyError) as e: + raise KeyError(str(e)) + assert False, "Should not be here" + + + def getCurrentAttribute(self): + return self.__currentAttribute + + + def getRowIndex(self): + return self.__currentRowIndex + + def getRowList(self): + return self._rowList + + def getRowCount(self): + return (len(self._rowList)) + + def getRow(self,index): + try: + return self._rowList[index] + except: + return [] + + def removeRow(self,index): + try: + if ((index >= 0) and (index < len(self._rowList))): + del self._rowList[index] + if self.__currentRowIndex >= len(self._rowList): + self.__currentRowIndex = len(self._rowList) -1 + return True + else: + pass + except: + pass + + return False + + def getFullRow(self,index): + """ Return a full row based on the length of the the attribute list. + """ + try: + if (len(self._rowList[index]) < self._numAttributes): + for ii in range( self._numAttributes-len(self._rowList[index])): + self._rowList[index].append('?') + return self._rowList[index] + except: + return ['?' for ii in range(self._numAttributes)] + + def getName(self): + return self._name + + def getAttributeList(self): + return self._attributeNameList + + def getAttributeCount(self): + return len(self._attributeNameList) + + def getAttributeListWithOrder(self): + oL=[] + for ii,att in enumerate(self._attributeNameList): + oL.append((att,ii)) + return oL + + def getAttributeIndex(self,attributeName): + try: + return self._attributeNameList.index(attributeName) + except: + return -1 + + def hasAttribute(self,attributeName): + return attributeName in self._attributeNameList + + def getIndex(self,attributeName): + try: + return self._attributeNameList.index(attributeName) + except: + return -1 + + def getItemNameList(self): + itemNameList=[] + for att in self._attributeNameList: + itemNameList.append("_"+self._name+"."+att) + return itemNameList + + def append(self,row): + #self.__lfh.write("PdbxContainer(append) category %s row %r\n" % (self._name,row)) + self._rowList.append(row) + + def appendAttribute(self,attributeName): + attributeNameLC = attributeName.lower() + if attributeNameLC in self._catalog: + i = self._attributeNameList.index(self._catalog[attributeNameLC]) + self._attributeNameList[i] = attributeName + self._catalog[attributeNameLC] = attributeName + #self.__lfh.write("Appending existing attribute %s\n" % attributeName) + else: + #self.__lfh.write("Appending existing attribute %s\n" % attributeName) + self._attributeNameList.append(attributeName) + self._catalog[attributeNameLC] = attributeName + # + self._numAttributes = len(self._attributeNameList) + + + def appendAttributeExtendRows(self,attributeName): + attributeNameLC = attributeName.lower() + if attributeNameLC in self._catalog: + i = self._attributeNameList.index(self._catalog[attributeNameLC]) + self._attributeNameList[i] = attributeName + self._catalog[attributeNameLC] = attributeName + self.__lfh.write("Appending existing attribute %s\n" % attributeName) + else: + self._attributeNameList.append(attributeName) + self._catalog[attributeNameLC] = attributeName + # add a placeholder to any existing rows for the new attribute. + if (len(self._rowList) > 0): + for row in self._rowList: + row.append("?") + # + self._numAttributes = len(self._attributeNameList) + + + + def getValue(self,attributeName=None,rowIndex=None): + if attributeName is None: + attribute = self.__currentAttribute + else: + attribute = attributeName + if rowIndex is None: + rowI = self.__currentRowIndex + else: + rowI =rowIndex + + if isinstance(attribute, str) and isinstance(rowI,int): + try: + return self._rowList[rowI][self._attributeNameList.index(attribute)] + except (IndexError): + raise IndexError + raise IndexError(str(attribute)) + + def setValue(self,value,attributeName=None,rowIndex=None): + if attributeName is None: + attribute=self.__currentAttribute + else: + attribute=attributeName + + if rowIndex is None: + rowI = self.__currentRowIndex + else: + rowI = rowIndex + + if isinstance(attribute, str) and isinstance(rowI,int): + try: + # if row index is out of range - add the rows - + for ii in range(rowI+1 - len(self._rowList)): + self._rowList.append(self.__emptyRow()) + # self._rowList[rowI][attribute]=value + ll=len(self._rowList[rowI]) + ind=self._attributeNameList.index(attribute) + + # extend the list if needed - + if ( ind >= ll): + self._rowList[rowI].extend([None for ii in range(2*ind -ll)]) + self._rowList[rowI][ind]=value + except (IndexError): + self.__lfh.write("DataCategory(setvalue) index error category %s attribute %s index %d value %r\n" % + (self._name,attribute,rowI,value)) + traceback.print_exc(file=self.__lfh) + #raise IndexError + except (ValueError): + self.__lfh.write("DataCategory(setvalue) value error category %s attribute %s index %d value %r\n" % + (self._name,attribute,rowI,value)) + traceback.print_exc(file=self.__lfh) + #raise ValueError + + def __emptyRow(self): + return [None for ii in range(len(self._attributeNameList))] + + def replaceValue(self,oldValue,newValue,attributeName): + numReplace=0 + if attributeName not in self._attributeNameList: + return numReplace + ind=self._attributeNameList.index(attributeName) + for row in self._rowList: + if row[ind] == oldValue: + row[ind]=newValue + numReplace += 1 + return numReplace + + def replaceSubstring(self,oldValue,newValue,attributeName): + ok=False + if attributeName not in self._attributeNameList: + return ok + ind=self._attributeNameList.index(attributeName) + for row in self._rowList: + val=row[ind] + row[ind]=val.replace(oldValue,newValue) + if val != row[ind]: + ok=True + return ok + + def invokeAttributeMethod(self,attributeName,type,method,db): + self.__currentRowIndex = 0 + self.__currentAttribute=attributeName + self.appendAttribute(attributeName) + # + ind=self._attributeNameList.index(attributeName) + if len(self._rowList) == 0: + row=[None for ii in range(len(self._attributeNameList)*2)] + row[ind]=None + self._rowList.append(row) + + for row in self._rowList: + ll = len(row) + if (ind >= ll): + row.extend([None for ii in range(2*ind-ll)]) + row[ind]=None + exec(method.getInline()) + self.__currentRowIndex+=1 + + def invokeCategoryMethod(self,type,method,db): + self.__currentRowIndex = 0 + exec(method.getInline()) + + def getAttributeLengthMaximumList(self): + mList=[0 for i in len(self._attributeNameList)] + for row in self._rowList: + for indx,val in enumerate(row): + mList[indx] = max(mList[indx],len(val)) + return mList + + def renameAttribute(self,curAttributeName,newAttributeName): + """ Change the name of an attribute in place - + """ + try: + i=self._attributeNameList.index(curAttributeName) + self._attributeNameList[i]=newAttributeName + del self._catalog[curAttributeName.lower()] + self._catalog[newAttributeName.lower()]=newAttributeName + return True + except: + return False + + def printIt(self,fh=sys.stdout): + fh.write("--------------------------------------------\n") + fh.write(" Category: %s attribute list length: %d\n" % + (self._name,len(self._attributeNameList))) + for at in self._attributeNameList: + fh.write(" Category: %s attribute: %s\n" % (self._name,at)) + + fh.write(" Row value list length: %d\n" % len(self._rowList)) + # + for row in self._rowList[:2]: + # + if len(row) == len(self._attributeNameList): + for ii,v in enumerate(row): + fh.write(" %30s: %s ...\n" % (self._attributeNameList[ii],str(v)[:30])) + else: + fh.write("+WARNING - %s data length %d attribute name length %s mismatched\n" % + (self._name,len(row),len(self._attributeNameList))) + + def dumpIt(self,fh=sys.stdout): + fh.write("--------------------------------------------\n") + fh.write(" Category: %s attribute list length: %d\n" % + (self._name,len(self._attributeNameList))) + for at in self._attributeNameList: + fh.write(" Category: %s attribute: %s\n" % (self._name,at)) + + fh.write(" Value list length: %d\n" % len(self._rowList)) + for row in self._rowList: + for ii,v in enumerate(row): + fh.write(" %30s: %s\n" % (self._attributeNameList[ii],v)) + + + def __formatPdbx(self, inp): + """ Format input data following PDBx quoting rules - + """ + try: + if (inp is None): + return ("?",'DT_NULL_VALUE') + + # pure numerical values are returned as unquoted strings + if (isinstance(inp,int) or self.__intRe.search(str(inp))): + return ( [str(inp)],'DT_INTEGER') + + if (isinstance(inp,float) or self.__floatRe.search(str(inp))): + return ([str(inp)],'DT_FLOAT') + + # null value handling - + + if (inp == "." or inp == "?"): + return ([inp],'DT_NULL_VALUE') + + if (inp == ""): + return (["."],'DT_NULL_VALUE') + + # Contains white space or quotes ? + if not self.__wsAndQuotesRe.search(inp): + if inp.startswith("_"): + return (self.__doubleQuotedList(inp),'DT_ITEM_NAME') + else: + return ([str(inp)],'DT_UNQUOTED_STRING') + else: + if self.__nlRe.search(inp): + return (self.__semiColonQuotedList(inp),'DT_MULTI_LINE_STRING') + else: + if (self.__avoidEmbeddedQuoting): + # change priority to choose double quoting where possible. + if not self.__dqRe.search(inp) and not self.__sqWsRe.search(inp): + return (self.__doubleQuotedList(inp),'DT_DOUBLE_QUOTED_STRING') + elif not self.__sqRe.search(inp) and not self.__dqWsRe.search(inp): + return (self.__singleQuotedList(inp),'DT_SINGLE_QUOTED_STRING') + else: + return (self.__semiColonQuotedList(inp),'DT_MULTI_LINE_STRING') + else: + # change priority to choose double quoting where possible. + if not self.__dqRe.search(inp): + return (self.__doubleQuotedList(inp),'DT_DOUBLE_QUOTED_STRING') + elif not self.__sqRe.search(inp): + return (self.__singleQuotedList(inp),'DT_SINGLE_QUOTED_STRING') + else: + return (self.__semiColonQuotedList(inp),'DT_MULTI_LINE_STRING') + + + except: + traceback.print_exc(file=self.__lfh) + + def __dataTypePdbx(self, inp): + """ Detect the PDBx data type - + """ + if (inp is None): + return ('DT_NULL_VALUE') + + # pure numerical values are returned as unquoted strings + if isinstance(inp,int) or self.__intRe.search(str(inp)): + return ('DT_INTEGER') + + if isinstance(inp,float) or self.__floatRe.search(str(inp)): + return ('DT_FLOAT') + + # null value handling - + + if (inp == "." or inp == "?"): + return ('DT_NULL_VALUE') + + if (inp == ""): + return ('DT_NULL_VALUE') + + # Contains white space or quotes ? + if not self.__wsAndQuotesRe.search(inp): + if inp.startswith("_"): + return ('DT_ITEM_NAME') + else: + return ('DT_UNQUOTED_STRING') + else: + if self.__nlRe.search(inp): + return ('DT_MULTI_LINE_STRING') + else: + if (self.__avoidEmbeddedQuoting): + if not self.__sqRe.search(inp) and not self.__dqWsRe.search(inp): + return ('DT_DOUBLE_QUOTED_STRING') + elif not self.__dqRe.search(inp) and not self.__sqWsRe.search(inp): + return ('DT_SINGLE_QUOTED_STRING') + else: + return ('DT_MULTI_LINE_STRING') + else: + if not self.__sqRe.search(inp): + return ('DT_DOUBLE_QUOTED_STRING') + elif not self.__dqRe.search(inp): + return ('DT_SINGLE_QUOTED_STRING') + else: + return ('DT_MULTI_LINE_STRING') + + def __singleQuotedList(self,inp): + l=[] + l.append("'") + l.append(inp) + l.append("'") + return(l) + + def __doubleQuotedList(self,inp): + l=[] + l.append('"') + l.append(inp) + l.append('"') + return(l) + + def __semiColonQuotedList(self,inp): + l=[] + l.append("\n") + if inp[-1] == '\n': + l.append(";") + l.append(inp) + l.append(";") + l.append("\n") + else: + l.append(";") + l.append(inp) + l.append("\n") + l.append(";") + l.append("\n") + + return(l) + + def getValueFormatted(self,attributeName=None,rowIndex=None): + if attributeName is None: + attribute=self.__currentAttribute + else: + attribute=attributeName + + if rowIndex is None: + rowI = self.__currentRowIndex + else: + rowI = rowIndex + + if isinstance(attribute, str) and isinstance(rowI,int): + try: + list,type=self.__formatPdbx(self._rowList[rowI][self._attributeNameList.index(attribute)]) + return "".join(list) + except (IndexError): + self.__lfh.write("attributeName %s rowI %r rowdata %r\n" % (attributeName,rowI,self._rowList[rowI])) + raise IndexError + raise TypeError(str(attribute)) + + + def getValueFormattedByIndex(self,attributeIndex,rowIndex): + try: + list,type=self.__formatPdbx(self._rowList[rowIndex][attributeIndex]) + return "".join(list) + except (IndexError): + raise IndexError + + def getAttributeValueMaxLengthList(self,steps=1): + mList=[0 for i in range(len(self._attributeNameList))] + for row in self._rowList[::steps]: + for indx in range(len(self._attributeNameList)): + val=row[indx] + mList[indx] = max(mList[indx],len(str(val))) + return mList + + def getFormatTypeList(self,steps=1): + try: + curDataTypeList=['DT_NULL_VALUE' for i in range(len(self._attributeNameList))] + for row in self._rowList[::steps]: + for indx in range(len(self._attributeNameList)): + val=row[indx] + # print "index ",indx," val ",val + dType=self.__dataTypePdbx(val) + dIndx=self.__dataTypeList.index(dType) + # print "d type", dType, " d type index ",dIndx + + cType=curDataTypeList[indx] + cIndx=self.__dataTypeList.index(cType) + cIndx= max(cIndx,dIndx) + curDataTypeList[indx]=self.__dataTypeList[cIndx] + + # Map the format types to the data types + curFormatTypeList=[] + for dt in curDataTypeList: + ii=self.__dataTypeList.index(dt) + curFormatTypeList.append(self.__formatTypeList[ii]) + except: + self.__lfh.write("PdbxDataCategory(getFormatTypeList) ++Index error at index %d in row %r\n" % (indx,row)) + + return curFormatTypeList,curDataTypeList + + def getFormatTypeListX(self): + curDataTypeList=['DT_NULL_VALUE' for i in range(len(self._attributeNameList))] + for row in self._rowList: + for indx in range(len(self._attributeNameList)): + val=row[indx] + #print "index ",indx," val ",val + dType=self.__dataTypePdbx(val) + dIndx=self.__dataTypeList.index(dType) + #print "d type", dType, " d type index ",dIndx + + cType=curDataTypeList[indx] + cIndx=self.__dataTypeList.index(cType) + cIndx= max(cIndx,dIndx) + curDataTypeList[indx]=self.__dataTypeList[cIndx] + + # Map the format types to the data types + curFormatTypeList=[] + for dt in curDataTypeList: + ii=self.__dataTypeList.index(dt) + curFormatTypeList.append(self.__formatTypeList[ii]) + return curFormatTypeList,curDataTypeList + + diff --git a/pmx/workflow/parmed/formats/pdbx/PdbxReader.py b/pmx/workflow/parmed/formats/pdbx/PdbxReader.py new file mode 100644 index 00000000..c87f3eb4 --- /dev/null +++ b/pmx/workflow/parmed/formats/pdbx/PdbxReader.py @@ -0,0 +1,447 @@ +## +# File: PdbxReader.py +# Date: 2012-01-09 Jdw Adapted from PdbxParser +# +# Updates: +# +# 2012-01-09 - (jdw) Separate reader and writer classes. +# +# 2012-09-02 - (jdw) Revise tokenizer to better handle embedded quoting. +# +## +""" +PDBx/mmCIF dictionary and data file parser. + +Acknowledgements: + + The tokenizer used in this module is modeled after the clever parser design + used in the PyMMLIB package. + + PyMMLib Development Group + Authors: Ethan Merritt: merritt@u.washington.ed & Jay Painter: jay.painter@gmail.com + See: http://pymmlib.sourceforge.net/ + +""" + +import re +from ...exceptions import PdbxError, PdbxSyntaxError +from .PdbxContainers import (DataCategory, DefinitionContainer, DataContainer) + +class PdbxReader(object): + """ PDBx reader for data files and dictionaries. + + """ + def __init__(self,ifh): + """ ifh - input file handle returned by open() + """ + # + self.__curLineNumber = 0 + self.__ifh=ifh + self.__stateDict={"data": "ST_DATA_CONTAINER", + "loop": "ST_TABLE", + "global": "ST_GLOBAL_CONTAINER", + "save": "ST_DEFINITION", + "stop": "ST_STOP"} + + def read(self, containerList): + """ + Appends to the input list of definition and data containers. + + """ + self.__curLineNumber = 0 + try: + self.__parser(self.__tokenizer(self.__ifh), containerList) + except StopIteration: + pass + except (RuntimeError, DeprecationWarning) as e: + if 'StopIteration' not in str(e): + raise + else: + raise PdbxError() + + def __syntaxError(self, errText): + raise PdbxSyntaxError(self.__curLineNumber, errText) + + def __getContainerName(self,inWord): + """ Returns the name of the data_ or save_ container + """ + return str(inWord[5:]).strip() + + def __getState(self, inWord): + """Identifies reserved syntax elements and assigns an associated state. + + Returns: (reserved word, state) + where - + reserved word - is one of CIF syntax elements: + data_, loop_, global_, save_, stop_ + state - the parser state required to process this next section. + """ + i = inWord.find("_") + if i == -1: + return None,"ST_UNKNOWN" + + try: + rWord=inWord[:i].lower() + return rWord, self.__stateDict[rWord] + except: + return None,"ST_UNKNOWN" + + def __parser(self, tokenizer, containerList): + """ Parser for PDBx data files and dictionaries. + + Input - tokenizer() reentrant method recognizing data item names (_category.attribute) + quoted strings (single, double and multi-line semi-colon delimited), and unquoted + strings. + + containerList - list-type container for data and definition objects parsed from + from the input file. + + Return: + containerList - is appended with data and definition objects - + """ + # Working container - data or definition + curContainer = None + # + # Working category container + categoryIndex = {} + curCategory = None + # + curRow = None + state = None + + # Find the first reserved word and begin capturing data. + # + while True: + curCatName, curAttName, curQuotedString, curWord = next(tokenizer) + if curWord is None: + continue + reservedWord, state = self.__getState(curWord) + if reservedWord is not None: + break + + while True: + # + # Set the current state - + # + # At this point in the processing cycle we are expecting a token containing + # either a '_category.attribute' or a reserved word. + # + if curCatName is not None: + state = "ST_KEY_VALUE_PAIR" + elif curWord is not None: + reservedWord, state = self.__getState(curWord) + else: + self.__syntaxError("Miscellaneous syntax error") + return + + # + # Process _category.attribute value assignments + # + if state == "ST_KEY_VALUE_PAIR": + try: + curCategory = categoryIndex[curCatName] + except KeyError: + # A new category is encountered - create a container and add a row + curCategory = categoryIndex[curCatName] = DataCategory(curCatName) + + try: + curContainer.append(curCategory) + except AttributeError: + self.__syntaxError("Category cannot be added to data_ block") + return + + curRow = [] + curCategory.append(curRow) + else: + # Recover the existing row from the category + try: + curRow = curCategory[0] + except IndexError: + self.__syntaxError("Internal index error accessing category data") + return + + # Check for duplicate attributes and add attribute to table. + if curAttName in curCategory.getAttributeList(): + self.__syntaxError("Duplicate attribute encountered in category") + return + else: + curCategory.appendAttribute(curAttName) + + + # Get the data for this attribute from the next token + tCat, tAtt, curQuotedString, curWord = next(tokenizer) + + if tCat is not None or (curQuotedString is None and curWord is None): + self.__syntaxError("Missing data for item _%s.%s" % (curCatName,curAttName)) + + if curWord is not None: + # + # Validation check token for misplaced reserved words - + # + reservedWord, state = self.__getState(curWord) + if reservedWord is not None: + self.__syntaxError("Unexpected reserved word: %s" % (reservedWord)) + + curRow.append(curWord) + + elif curQuotedString is not None: + curRow.append(curQuotedString) + + else: + self.__syntaxError("Missing value in item-value pair") + + curCatName, curAttName, curQuotedString, curWord = next(tokenizer) + continue + + # + # Process a loop_ declaration and associated data - + # + elif state == "ST_TABLE": + + # The category name in the next curCatName,curAttName pair + # defines the name of the category container. + curCatName,curAttName,curQuotedString,curWord = next(tokenizer) + + if curCatName is None or curAttName is None: + self.__syntaxError("Unexpected token in loop_ declaration") + return + + # Check for a previous category declaration. + if curCatName in categoryIndex: + self.__syntaxError("Duplicate category declaration in loop_") + return + + curCategory = DataCategory(curCatName) + + try: + curContainer.append(curCategory) + except AttributeError: + self.__syntaxError("loop_ declaration outside of data_ block or save_ frame") + return + + curCategory.appendAttribute(curAttName) + + # Read the rest of the loop_ declaration + while True: + curCatName, curAttName, curQuotedString, curWord = next(tokenizer) + + if curCatName is None: + break + + if curCatName != curCategory.getName(): + self.__syntaxError("Changed category name in loop_ declaration") + return + + curCategory.appendAttribute(curAttName) + + + # If the next token is a 'word', check it for any reserved words - + if curWord is not None: + reservedWord, state = self.__getState(curWord) + if reservedWord is not None: + if reservedWord == "stop": + return + else: + self.__syntaxError("Unexpected reserved word after loop declaration: %s" % (reservedWord)) + + # Read the table of data for this loop_ - + while True: + curRow = [] + curCategory.append(curRow) + + for tAtt in curCategory.getAttributeList(): + if curWord is not None: + curRow.append(curWord) + elif curQuotedString is not None: + curRow.append(curQuotedString) + + curCatName,curAttName,curQuotedString,curWord = next(tokenizer) + + # loop_ data processing ends if - + + # A new _category.attribute is encountered + if curCatName is not None: + break + + # A reserved word is encountered + if curWord is not None: + reservedWord, state = self.__getState(curWord) + if reservedWord is not None: + break + + continue + + + elif state == "ST_DEFINITION": + # Ignore trailing unnamed saveframe delimiters e.g. 'save_' + sName=self.__getContainerName(curWord) + if (len(sName) > 0): + curContainer = DefinitionContainer(sName) + containerList.append(curContainer) + categoryIndex = {} + curCategory = None + + curCatName,curAttName,curQuotedString,curWord = next(tokenizer) + + elif state == "ST_DATA_CONTAINER": + # + dName=self.__getContainerName(curWord) + if len(dName) == 0: + dName="unidentified" + curContainer = DataContainer(dName) + containerList.append(curContainer) + categoryIndex = {} + curCategory = None + curCatName,curAttName,curQuotedString,curWord = next(tokenizer) + + elif state == "ST_STOP": + return + elif state == "ST_GLOBAL": + curContainer = DataContainer("blank-global") + curContainer.setGlobal() + containerList.append(curContainer) + categoryIndex = {} + curCategory = None + curCatName,curAttName,curQuotedString,curWord = next(tokenizer) + + elif state == "ST_UNKNOWN": + self.__syntaxError("Unrecogized syntax element: " + str(curWord)) + return + + + def __tokenizer(self, ifh): + """ Tokenizer method for the mmCIF syntax file - + + Each return/yield from this method returns information about + the next token in the form of a tuple with the following structure. + + (category name, attribute name, quoted strings, words w/o quotes or white space) + + Differentiated the regular expression to the better handle embedded quotes. + + """ + # + # Regex definition for mmCIF syntax - semi-colon delimited strings are handled + # outside of this regex. + mmcifRe = re.compile( + r"(?:" + + "(?:_(.+?)[.](\S+))" "|" # _category.attribute + + "(?:['](.*?)(?:[']\s|[']$))" "|" # single quoted strings + "(?:[\"](.*?)(?:[\"]\s|[\"]$))" "|" # double quoted strings + + "(?:\s*#.*$)" "|" # comments (dumped) + + "(\S+)" # unquoted words + + ")") + + fileIter = iter(ifh) + + ## Tokenizer loop begins here --- + while True: + line = next(fileIter) + self.__curLineNumber += 1 + + # Dump comments + if line.startswith("#"): + continue + + # Gobble up the entire semi-colon/multi-line delimited string and + # and stuff this into the string slot in the return tuple + # + if line.startswith(";"): + mlString = [line[1:]] + while True: + line = next(fileIter) + self.__curLineNumber += 1 + if line.startswith(";"): + break + mlString.append(line) + + # remove trailing new-line that is part of the \n; delimiter + mlString[-1] = mlString[-1].rstrip() + # + yield (None, None, "".join(mlString), None) + # + # Need to process the remainder of the current line - + line = line[1:] + #continue + + # Apply regex to the current line consolidate the single/double + # quoted within the quoted string category + for it in mmcifRe.finditer(line): + tgroups = it.groups() + if tgroups != (None, None, None, None, None): + if tgroups[2] is not None: + qs = tgroups[2] + elif tgroups[3] is not None: + qs = tgroups[3] + else: + qs = None + groups = (tgroups[0],tgroups[1],qs,tgroups[4]) + yield groups + + def __tokenizerOrg(self, ifh): + """ Tokenizer method for the mmCIF syntax file - + + Each return/yield from this method returns information about + the next token in the form of a tuple with the following structure. + + (category name, attribute name, quoted strings, words w/o quotes or white space) + + """ + # + # Regex definition for mmCIF syntax - semi-colon delimited strings are handled + # outside of this regex. + mmcifRe = re.compile( + r"(?:" + + "(?:_(.+?)[.](\S+))" "|" # _category.attribute + + "(?:['\"](.*?)(?:['\"]\s|['\"]$))" "|" # quoted strings + + "(?:\s*#.*$)" "|" # comments (dumped) + + "(\S+)" # unquoted words + + ")") + + fileIter = iter(ifh) + + ## Tokenizer loop begins here --- + while True: + line = next(fileIter) + self.__curLineNumber += 1 + + # Dump comments + if line.startswith("#"): + continue + + # Gobble up the entire semi-colon/multi-line delimited string and + # and stuff this into the string slot in the return tuple + # + if line.startswith(";"): + mlString = [line[1:]] + while True: + line = next(fileIter) + self.__curLineNumber += 1 + if line.startswith(";"): + break + mlString.append(line) + + # remove trailing new-line that is part of the \n; delimiter + mlString[-1] = mlString[-1].rstrip() + # + yield (None, None, "".join(mlString), None) + # + # Need to process the remainder of the current line - + line = line[1:] + #continue + + ## Apply regex to the current line + for it in mmcifRe.finditer(line): + groups = it.groups() + if groups != (None, None, None, None): + yield groups diff --git a/pmx/workflow/parmed/formats/pdbx/PdbxWriter.py b/pmx/workflow/parmed/formats/pdbx/PdbxWriter.py new file mode 100644 index 00000000..68616953 --- /dev/null +++ b/pmx/workflow/parmed/formats/pdbx/PdbxWriter.py @@ -0,0 +1,187 @@ +## +# File: PdbxWriter.py +# Date: 2011-10-09 Jdw Adapted from PdbxParser.py +# +# Updates: +# 5-Apr-2011 jdw Using the double quote format preference +# 23-Oct-2012 jdw update path details and reorganize. +# +### +""" +Classes for writing data and dictionary containers in PDBx/mmCIF format. + +""" +__docformat__ = "restructuredtext en" +__author__ = "John Westbrook" +__email__ = "jwest@rcsb.rutgers.edu" +__license__ = "Creative Commons Attribution 3.0 Unported" +__version__ = "V0.01" + +from .PdbxContainers import (DefinitionContainer, DataContainer) +from ...exceptions import PdbxError +import sys + +class PdbxWriter(object): + """Write PDBx data files or dictionaries using the input container + or container list. + """ + def __init__(self,ofh=sys.stdout): + self.__ofh=ofh + self.__containerList=[] + self.__MAXIMUM_LINE_LENGTH = 2048 + self.__SPACING = 2 + self.__INDENT_DEFINITION = 3 + self.__indentSpace = " " * self.__INDENT_DEFINITION + self.__doDefinitionIndent=False + # Maximum number of rows checked for value length and format + self.__rowPartition=None + + def setRowPartition(self,numRows): + ''' Maximum number of rows checked for value length and format + ''' + self.__rowPartition=numRows + + def write(self, containerList): + self.__containerList=containerList + for container in self.__containerList: + self.writeContainer(container) + + def writeContainer(self,container): + indS=" " * self.__INDENT_DEFINITION + if isinstance(container, DefinitionContainer): + self.__write("save_%s\n" % container.getName()) + self.__doDefinitionIndent=True + self.__write(indS+"#\n") + elif isinstance(container, DataContainer): + if (container.getGlobal()): + self.__write("global_\n") + self.__doDefinitionIndent=False + self.__write("\n") + else: + self.__write("data_%s\n" % container.getName()) + self.__doDefinitionIndent=False + self.__write("#\n") + + for nm in container.getObjNameList(): + obj=container.getObj(nm) + objL=obj.getRowList() + + # Skip empty objects + if len(objL) == 0: + continue + + # Item - value formattting + elif len(objL) == 1: + self.__writeItemValueFormat(obj) + + # Table formatting - + elif len(objL) > 1 and len(obj.getAttributeList()) > 0: + self.__writeTableFormat(obj) + else: + raise PdbxError() + + if self.__doDefinitionIndent: + self.__write(indS+"#") + else: + self.__write("#") + + # Add a trailing saveframe reserved word + if isinstance(container, DefinitionContainer): + self.__write("\nsave_\n") + self.__write("#\n") + + def __write(self, st): + self.__ofh.write(st) + + def __writeItemValueFormat(self, myCategory): + + # Compute the maximum item name length within this category - + attributeNameLengthMax = 0 + for attributeName in myCategory.getAttributeList(): + attributeNameLengthMax = max(attributeNameLengthMax, len(attributeName)) + itemNameLengthMax = self.__SPACING + len(myCategory.getName()) + attributeNameLengthMax + 2 + # + lineList=[] + lineList.append("#\n") + for attributeName,iPos in myCategory.getAttributeListWithOrder(): + if self.__doDefinitionIndent: + # - add indent -- + lineList.append(self.__indentSpace) + + itemName = "_%s.%s" % (myCategory.getName(), attributeName) + lineList.append(itemName.ljust(itemNameLengthMax)) + + lineList.append(myCategory.getValueFormatted(attributeName,0)) + lineList.append("\n") + + self.__write("".join(lineList)) + + def __writeTableFormat(self, myCategory): + + # Write the declaration of the loop_ + # + lineList=[] + lineList.append('#\n') + if self.__doDefinitionIndent: + lineList.append(self.__indentSpace) + lineList.append("loop_") + for attributeName in myCategory.getAttributeList(): + lineList.append('\n') + if self.__doDefinitionIndent: + lineList.append(self.__indentSpace) + itemName = "_%s.%s" % (myCategory.getName(), attributeName) + lineList.append(itemName) + self.__write("".join(lineList)) + + # + # Write the data in tabular format - + # + #print myCategory.getName() + #print myCategory.getAttributeList() + + # For speed make the following evaluation on a portion of the table + if self.__rowPartition is not None: + numSteps=max(1,myCategory.getRowCount()/self.__rowPartition) + else: + numSteps=1 + + formatTypeList,dataTypeList=myCategory.getFormatTypeList(steps=numSteps) + maxLengthList=myCategory.getAttributeValueMaxLengthList(steps=numSteps) + spacing = " " * self.__SPACING + # + + #print formatTypeList + #print dataTypeList + #print maxLengthList + # + for iRow in range(myCategory.getRowCount()): + lineList = [] + lineList.append('\n') + if self.__doDefinitionIndent: + lineList.append(self.__indentSpace + " ") + + for iAt in range(myCategory.getAttributeCount()): + formatType = formatTypeList[iAt] + maxLength = maxLengthList[iAt] + + if (formatType == 'FT_UNQUOTED_STRING' or formatType == 'FT_NULL_VALUE'): + val=myCategory.getValueFormattedByIndex(iAt,iRow) + lineList.append(val.ljust(maxLength)) + + elif formatType == 'FT_NUMBER': + val=myCategory.getValueFormattedByIndex(iAt,iRow) + lineList.append(val.rjust(maxLength)) + + elif formatType == 'FT_QUOTED_STRING': + val=myCategory.getValueFormattedByIndex(iAt,iRow) + + lineList.append(val.ljust(maxLength+2)) + + elif formatType == "FT_MULTI_LINE_STRING": + val=myCategory.getValueFormattedByIndex(iAt,iRow) + lineList.append(val) + + lineList.append(spacing) + + self.__write("".join(lineList)) + self.__write("\n") diff --git a/pmx/workflow/parmed/formats/pdbx/__init__.py b/pmx/workflow/parmed/formats/pdbx/__init__.py new file mode 100644 index 00000000..1626064b --- /dev/null +++ b/pmx/workflow/parmed/formats/pdbx/__init__.py @@ -0,0 +1,17 @@ +""" +The PDBx package here was taken from the wwPDB website written by John Westbook +(and modified slightly to fix issues with Python 3 support as well as unified +exception handling) +""" + +__author__ = "John Westbrook" +__contributors__ = "Jason Swails" +__version__ = "V0.01" +__license__ = "Creative Commons Attribution 3.0 Unported" +__email__ = "jwest@rcsb.rutgers.edu jason.swails@gmail.com" + +__all__ = ['PdbxReader', 'PdbxWriter', 'containers'] + +from .PdbxReader import PdbxReader +from .PdbxWriter import PdbxWriter +from . import PdbxContainers as containers diff --git a/pmx/workflow/parmed/formats/pqr.py b/pmx/workflow/parmed/formats/pqr.py new file mode 100644 index 00000000..2b99f8b9 --- /dev/null +++ b/pmx/workflow/parmed/formats/pqr.py @@ -0,0 +1,313 @@ +""" +This module contains classes for reading and writing PQR files +""" +from contextlib import closing +import numpy as np +from .registry import FileFormatType +from .pdb import _standardize_resname, PDBFile, _is_hetatm +from ..exceptions import PDBError, PDBWarning +from ..periodic_table import AtomicNum, Mass, Element, element_by_name +from ..structure import Structure +from ..topologyobjects import Atom, ExtraPoint +from ..utils.io import genopen +import warnings + +class PQRFile(metaclass=FileFormatType): + """ Standard PDB file format parser and writer """ + #=================================================== + + @staticmethod + def id_format(filename): + """ Identifies the file type as a PDB file + + Parameters + ---------- + filename : str + Name of the file to check format for + + Returns + ------- + is_fmt : bool + True if it is a PQR file + """ + with closing(genopen(filename, 'r')) as f: + for line in f: + words = line.split() + if not words: + continue + elif words[0] in ('CRYST1', 'END', 'END', 'HEADER', 'NUMMDL', + 'MASTER', 'AUTHOR', 'CAVEAT', 'COMPND', 'EXPDTA', + 'MDLTYP', 'KEYWDS', 'OBSLTE', 'SOURCE', 'SPLIT', + 'SPRSDE', 'TITLE ', 'ANISOU', 'CISPEP', 'CONECT', + 'DBREF ', 'HELIX ', 'HET', 'LINK', 'MODRES', + 'REVDAT', 'SEQADV', 'SHEET', 'SSBOND', 'FORMUL', + 'HETNAM', 'HETSYN', 'SEQRES', 'SITE', 'ENDMDL', 'MODEL', + 'JRNL', 'REMARK', 'TER', 'USER'): + continue + elif line[:5] in ('ORIGX', 'SCALE', 'MTRIX'): + if line[5] not in '123': + return False + elif words[0] in ('ATOM', 'HETATM'): + # Format is: + # rec atnum atname resname [chain] resnum x y z chg radius + # Where the chain ID is optional. rec must be ATOM or HETATM + if len(words) < 10: + return False + elif PDBFile.id_format(filename): + return False # It is a PDB file + + if len(words) == 10: + offset = 0 + elif len(words) >= 11: + offset = 1 + try: + float(words[10]) + except ValueError: + offset = 0 + if not words[1].isdigit(): return False + if words[2].isdigit(): return False + if words[3].isdigit(): return False + if not words[4+offset].isdigit(): return False + try: + float(words[5+offset]) + float(words[6+offset]) + float(words[7+offset]) + float(words[8+offset]) + float(words[9+offset]) + except ValueError: + return False + return True + else: + return False + return False + + #=================================================== + + @staticmethod + def parse(filename, skip_bonds=True): + """ Read a PQR file and return a populated `Structure` class + + Parameters + ---------- + filename : str or file-like + Name of the PQR file to read, or a file-like object that can iterate + over the lines of a PQR. Compressed file names can be specified and + are determined by file-name extension (e.g., file.pqr.gz, + file.pqr.bz2) + skip_bonds : bool, optional + If True, skip trying to assign bonds. This can save substantial time + when parsing large files with non-standard residue names. However, + no bonds are assigned. This is OK if, for instance, the PQR file is + being parsed simply for its coordinates. Default is False. + + Returns + ------- + structure : :class:`Structure` + The Structure object initialized with all of the information from + the PDB file. No bonds or other topological features are added by + default. + """ + if isinstance(filename, str): + own_handle = True + fileobj = genopen(filename, 'r') + else: + own_handle = False + fileobj = filename + + struct = Structure() + # Add metadata fields + modelno = 1 # For PDB files with multiple MODELs + atomno = 0 + coordinates = [] + all_coordinates = [] + + # Support hexadecimal numbering like that printed by VMD + try: + for line in fileobj: + words = line.split() + if words[0] in ('ATOM', 'HETATM'): + atomno += 1 + if len(words) == 10: + _, num, nam, res, resn, x, y, z, chg, rad = words + chn = '' + elif len(words) >= 11: + _, num, nam, res, chn, resn, x, y, z, chg, rad = ( + words[i] for i in range(11) + ) + # If the radius is not a float (but rather a letter, + # like the element or something), then the chain might + # be missing. In this case, shift all tokens "back" one + # and empty the chn string + try: + float(rad) + except ValueError: + resn, x, y, z, chg, rad = chn, resn, x, y, z, chg + else: + raise ValueError('Illegal PQR record format: expected ' + '10 or 11 tokens on the atom line') + x, y, z = float(x), float(y), float(z) + chg, rad = float(chg), float(rad) + resn, num = int(resn), int(num) + elem = element_by_name(nam) # Yuck + atomic_number = AtomicNum[elem] + mass = Mass[elem] + if nam in ('EP', 'LP'): # lone pair + atom = ExtraPoint(atomic_number=atomic_number, name=nam, + charge=chg, mass=mass, number=num, + solvent_radius=rad) + else: + atom = Atom(atomic_number=atomic_number, name=nam, + charge=chg, mass=mass, number=num, + solvent_radius=rad) + atom.xx, atom.xy, atom.xz = float(x), float(y), float(z) + if modelno == 1: + struct.add_atom(atom, res, resn, chn) + else: + try: + orig_atom = struct.atoms[atomno-1] + except IndexError: + raise PDBError('Extra atom in MODEL %d' % modelno) + if (orig_atom.residue.name != res.strip() + or orig_atom.name != nam.strip()): + raise PDBError( + f'Atom {atomno} differs in MODEL {modelno} [{orig_atom.residue.name} ' + f'{orig_atom.name} vs. {res} {nam}]' + ) + coordinates.extend([atom.xx, atom.xy, atom.xz]) + elif words[0] == 'ENDMDL': + # End the current model + if len(struct.atoms) == 0: + raise PDBError('MODEL ended before any atoms read in') + modelno += 1 + if len(struct.atoms)*3 != len(coordinates): + raise PDBError('Inconsistent atom numbers in some PDB models') + all_coordinates.append(coordinates) + atomno = 0 + coordinates = [] + elif words[0] == 'MODEL': + if modelno == 1 and len(struct.atoms) == 0: continue + if len(coordinates) > 0: + if len(struct.atoms)*3 != len(coordinates): + raise PDBError('Inconsistent atom numbers in some PDB models') + warnings.warn('MODEL not explicitly ended', PDBWarning) + all_coordinates.append(coordinates) + coordinates = [] + modelno += 1 + atomno = 0 + elif words[0] == 'CRYST1': + a, b, c = (float(w) for w in words[1:4]) + try: + A, B, C = (float(w) for w in words[4:7]) + except ValueError: + A = B = C = 90.0 + struct.box = [a, b, c, A, B, C] + finally: + if own_handle: + fileobj.close() + + struct.unchange() + if not skip_bonds: + struct.assign_bonds() + if coordinates: + if len(coordinates) != 3 * len(struct.atoms): + raise PDBError('bad number of atoms in some PQR models') + all_coordinates.append(coordinates) + struct._coordinates = np.array(all_coordinates).reshape((-1, len(struct.atoms), 3)) + return struct + + #=================================================== + + @staticmethod + def write(struct, dest, renumber=True, coordinates=None, + standard_resnames=False): + """ Write a PDB file from a Structure instance + + Parameters + ---------- + struct : :class:`Structure` + The structure from which to write the PDB file + dest : str or file-like + Either a file name or a file-like object containing a `write` + method to which to write the PDB file. If it is a filename that + ends with .gz or .bz2, a compressed version will be written using + either gzip or bzip2, respectively. + renumber : bool, optional + If True, renumber the atoms and residues sequentially as they are + stored in the structure. If False, use the original numbering if + it was assigned previously. Default is True + coordinates : array-like of float, optional + If provided, these coordinates will be written to the PDB file + instead of the coordinates stored in the structure. These + coordinates should line up with the atom order in the structure + (not necessarily the order of the "original" PDB file if they + differ) + standard_resnames : bool, optional + If True, common aliases for various amino and nucleic acid residues + will be converted into the PDB-standard values. Default is False + """ + own_handle = False + if not hasattr(dest, 'write'): + dest = genopen(dest, 'w') + own_handle = True + atomrec = ('ATOM %5d %-3s %-3s %1s %3d %7.3f %7.3f %7.3f %8.4f ' + '%8.4f\n') + hetatomrec = atomrec.replace('ATOM ', 'HETATM') + if struct.box is not None: + dest.write('CRYST1 %8.3f %8.3f %8.3f %6.2f %6.2f %6.2f\n' % ( + struct.box[0], struct.box[1], struct.box[2], struct.box[3], + struct.box[4], struct.box[5])) + if coordinates is not None: + coords = np.array(coordinates, copy=False, subok=True) + try: + coords = coords.reshape((-1, len(struct.atoms), 3)) + except ValueError: + raise TypeError("Coordinates has unexpected shape") + else: + coords = struct.get_coordinates('all') + # Create a function to process each atom and return which one we want + # to print, based on our alternate location choice + if standard_resnames: + standardize = lambda x: _standardize_resname(x) + else: + standardize = lambda x: (x, _is_hetatm(x)) + last_number = 0 + last_rnumber = 0 + for model, coord in enumerate(coords): + if coords.shape[0] > 1: + dest.write('MODEL %5d\n' % (model+1)) + for res in struct.residues: + if renumber: + atoms = res.atoms + else: + atoms = sorted(res.atoms, key=lambda atom: atom.number) + for atom in atoms: + # Figure out the serial numbers we want to print + if renumber: + anum = (atom.idx + 1) + rnum = (res.idx + 1) + else: + anum = (atom.number or last_number + 1) + rnum = (atom.residue.number or last_rnumber + 1) + last_number = anum + last_rnumber = rnum + # Do any necessary name munging to respect the PDB spec + if (len(atom.name) < 4 and + len(Element[atom.atomic_number]) != 2): + aname = ' %-3s' % atom.name + else: + aname = atom.name + xyz = coord[atom.idx] + resname, hetatm = standardize(res.name) + if hetatm: + rec = hetatomrec + else: + rec = atomrec + dest.write(rec % (anum, aname, resname, res.chain, rnum, + xyz[0], xyz[1], xyz[2], atom.charge, + atom.solvent_radius)) + if coords.shape[0] > 1: + dest.write('ENDMDL\n') + + dest.write("%-80s\n" % "END") + if own_handle: + dest.close() diff --git a/pmx/workflow/parmed/formats/psf.py b/pmx/workflow/parmed/formats/psf.py new file mode 100644 index 00000000..2bf02e15 --- /dev/null +++ b/pmx/workflow/parmed/formats/psf.py @@ -0,0 +1,285 @@ +""" +This module contains classes for reading and writing CHARMM- and XPLOR-style PSF +files +""" +# TODO -- move this functionality to a more centralized location +from contextlib import closing +from ..charmm.psf import CharmmPsfFile +from ..formats.registry import FileFormatType +from ..utils import tag_molecules +from ..utils.io import genopen + +class PSFFile(metaclass=FileFormatType): + """ + CHARMM- or XPLOR-style PSF file parser and writer. This class is + specifically a holder for the writing functionality and a vessel for + automatic file type detection. If you wish to instantiate a PSF file + directly, use :class:`parmed.charmm.CharmmPsfFile` or the + :func:`parmed.formats.load_file` function instead. + """ + + @staticmethod + def id_format(filename): + """ Identifies the file type as a CHARMM PSF file + + Parameters + ---------- + filename : str + Name of the file to check format for + + Returns + ------- + is_fmt : bool + True if it is a CHARMM or Xplor-style PSF file + """ + with closing(genopen(filename, 'r')) as f: + return f.readline().strip().startswith("PSF") + + @staticmethod + def parse(filename): + """ Read a CHARMM- or XPLOR-style PSF file + + Parameters + ---------- + filename : str + Name of the file to parse + + Returns + ------- + psf_file : :class:`CharmmPsfFile` + The PSF file instance with all information loaded + """ + return CharmmPsfFile(filename) + + @staticmethod + def write(struct, dest, vmd=False): + """ + Writes a PSF file from the stored molecule + + Parameters + ---------- + struct : :class:`Structure` + The Structure instance from which the PSF should be written + dest : str or file-like + The place to write the output PSF file. If it has a "write" + attribute, it will be used to print the PSF file. Otherwise, it will + be treated like a string and a file will be opened, printed, then + closed + vmd : bool + If True, it will write out a PSF in the format that VMD prints it in + (i.e., no NUMLP/NUMLPH or MOLNT sections) + + Examples + -------- + >>> cs = CharmmPsfFile('testfiles/test.psf') + >>> cs.write_psf('testfiles/test2.psf') + """ + # See if this is an extended format + try: + ext = 'EXT' in struct.flags + except AttributeError: + ext = True + # See if this is an XPLOR format + try: + xplor = 'XPLOR' in struct.flags + except AttributeError: + for atom in struct.atoms: + if isinstance(atom.type, str): + xplor = True + break + else: + xplor = False + own_handle = False + # Index the atoms and residues TODO delete + if isinstance(dest, str): + own_handle = True + dest = genopen(dest, 'w') + + # Assign the formats we need to write with + if ext: + atmfmt1 = ('%10d %-8s %-8i %-8s %-8s %6d %10.6f %13.4f' + 11*' ') + atmfmt2 = ('%10d %-8s %-8i %-8s %-8s %-6s %10.6f %13.4f' + 11*' ') + intfmt = '%10d' # For pointers + else: + atmfmt1 = ('%8d %-4s %-4i %-4s %-4s %4d %10.6f %13.4f' + 11*' ') + atmfmt2 = ('%8d %-4s %-4i %-4s %-4s %-4s %10.6f %13.4f' + 11*' ') + intfmt = '%8d' # For pointers + + # Now print the header then the title + dest.write('PSF CHEQ ') + if hasattr(struct, 'flags'): + dest.write(' '.join(f for f in struct.flags if f not in ('CHEQ',))) + else: + dest.write('EXT') # EXT is always active if no flags present + if xplor: + dest.write(' XPLOR') + dest.write('\n\n') + if isinstance(struct.title, str): + dest.write(intfmt % 1 + ' !NTITLE\n') + dest.write('%s\n\n' % struct.title) + else: + dest.write(intfmt % len(struct.title) + ' !NTITLE\n') + dest.write('\n'.join(struct.title) + '\n\n') + # Now time for the atoms + dest.write(intfmt % len(struct.atoms) + ' !NATOM\n') + # atmfmt1 is for CHARMM format (i.e., atom types are integers) + # atmfmt is for XPLOR format (i.e., atom types are strings) + add = 0 if struct.residues[0].number > 0 else 1-struct.residues[0].number + for i, atom in enumerate(struct.atoms): + typ = atom.type + if isinstance(atom.type, str): + fmt = atmfmt2 + if not atom.type: typ = atom.name + else: + fmt = atmfmt1 + segid = atom.residue.segid or 'SYS' + atmstr = fmt % (i+1, segid, atom.residue.number+add, + atom.residue.name, atom.name, typ, + atom.charge, atom.mass) + if hasattr(atom, 'props'): + dest.write(atmstr + ' '.join(atom.props) + '\n') + else: + dest.write('%s\n' % atmstr) + dest.write('\n') + # Bonds + dest.write(intfmt % len(struct.bonds) + ' !NBOND: bonds\n') + for i, bond in enumerate(struct.bonds): + dest.write((intfmt*2) % (bond.atom1.idx+1, bond.atom2.idx+1)) + if i % 4 == 3: # Write 4 bonds per line + dest.write('\n') + # See if we need to terminate + if len(struct.bonds) % 4 != 0 or len(struct.bonds) == 0: + dest.write('\n') + dest.write('\n') + # Angles + dest.write(intfmt % len(struct.angles) + ' !NTHETA: angles\n') + for i, angle in enumerate(struct.angles): + dest.write((intfmt*3) % (angle.atom1.idx+1, angle.atom2.idx+1, + angle.atom3.idx+1) + ) + if i % 3 == 2: # Write 3 angles per line + dest.write('\n') + # See if we need to terminate + if len(struct.angles) % 3 != 0 or len(struct.angles) == 0: + dest.write('\n') + dest.write('\n') + # Dihedrals + # impropers need to be split off in the "improper" section. + # PSF files need to have each dihedral listed *only* once. So count the + # number of unique dihedrals + nnormal = 0 + torsions = set() + for dih in struct.dihedrals: + if dih.improper: continue + a1, a2, a3, a4 = dih.atom1, dih.atom2, dih.atom3, dih.atom4 + if (a1, a2, a3, a4) in torsions or (a4, a3, a2, a1) in torsions: + continue + nnormal += 1 + torsions.add((a1, a2, a3, a4)) + nimprop = sum(1 for dih in struct.dihedrals if dih.improper) + dest.write(intfmt % nnormal + ' !NPHI: dihedrals\n') + torsions = set() + c = 0 + for dih in struct.dihedrals: + if dih.improper: continue + a1, a2, a3, a4 = dih.atom1, dih.atom2, dih.atom3, dih.atom4 + if (a1, a2, a3, a4) in torsions or (a4, a3, a2, a1) in torsions: + continue + dest.write((intfmt*4) % (a1.idx+1, a2.idx+1, a3.idx+1, a4.idx+1)) + torsions.add((a1, a2, a3, a4)) + if c % 2 == 1: # Write 2 dihedrals per line + dest.write('\n') + c += 1 + # See if we need to terminate + if nnormal % 2 != 0 or nnormal == 0: + dest.write('\n') + dest.write('\n') + # Impropers + nimprop += len(struct.impropers) + dest.write(intfmt % (nimprop) + ' !NIMPHI: impropers\n') + def improp_gen(struct): + for imp in struct.impropers: + yield (imp.atom1, imp.atom2, imp.atom3, imp.atom4) + for dih in struct.dihedrals: + if dih.improper: + yield (dih.atom1, dih.atom2, dih.atom3, dih.atom4) + for i, (a1, a2, a3, a4) in enumerate(improp_gen(struct)): + dest.write((intfmt*4) % (a1.idx+1, a2.idx+1, a3.idx+1, a4.idx+1)) + if i % 2 == 1: # Write 2 dihedrals per line + dest.write('\n') + # See if we need to terminate + if nimprop % 2 != 0 or nimprop == 0: + dest.write('\n') + dest.write('\n') + # Donor section + dest.write(intfmt % len(struct.donors) + ' !NDON: donors\n') + for i, don in enumerate(struct.donors): + dest.write((intfmt*2) % (don.atom1.idx+1, don.atom2.idx+1)) + if i % 4 == 3: # 4 donors per line + dest.write('\n') + if len(struct.donors) % 4 != 0 or len(struct.donors) == 0: + dest.write('\n') + dest.write('\n') + # Acceptor section + dest.write(intfmt % len(struct.acceptors) + ' !NACC: acceptors\n') + for i, acc in enumerate(struct.acceptors): + dest.write((intfmt*2) % (acc.atom1.idx+1, acc.atom2.idx+1)) + if i % 4 == 3: # 4 donors per line + dest.write('\n') + if len(struct.acceptors) % 4 != 0 or len(struct.acceptors) == 0: + dest.write('\n') + dest.write('\n') + # NNB section ?? + dest.write(intfmt % 0 + ' !NNB\n\n') + for i in range(len(struct.atoms)): + dest.write(intfmt % 0) + if i % 8 == 7: # Write 8 0's per line + dest.write('\n') + if len(struct.atoms) % 8 != 0: dest.write('\n') + dest.write('\n') + # Group section + try: + nst2 = struct.groups.nst2 + except AttributeError: + nst2 = 0 + dest.write((intfmt*2) % (len(struct.groups) or 1, nst2)) + dest.write(' !NGRP NST2\n') + if struct.groups: + for i, gp in enumerate(struct.groups): + dest.write((intfmt*3) % (gp.atom.idx, gp.type, gp.move)) + if i % 3 == 2: dest.write('\n') + if len(struct.groups) % 3 != 0 or len(struct.groups) == 0: + dest.write('\n') + else: + typ = 1 if abs(sum(a.charge for a in struct.atoms)) < 1e-4 else 2 + dest.write((intfmt*3) % (0, typ, 0)) + dest.write('\n') + dest.write('\n') + # The next two sections are never found in VMD prmtops... + if not vmd: + # Molecule section; first set molecularity + tag_molecules(struct) + mollist = [a.marked for a in struct.atoms] + dest.write(intfmt % max(mollist) + ' !MOLNT\n') + for i, atom in enumerate(struct.atoms): + dest.write(intfmt % atom.marked) + if i % 8 == 7: dest.write('\n') + if len(struct.atoms) % 8 != 0: dest.write('\n') + dest.write('\n') + # NUMLP/NUMLPH section + dest.write((intfmt*2) % (0, 0) + ' !NUMLP NUMLPH\n') + dest.write('\n') + # CMAP section + dest.write(intfmt % len(struct.cmaps) + ' !NCRTERM: cross-terms\n') + for i, cmap in enumerate(struct.cmaps): + dest.write((intfmt*8) % (cmap.atom1.idx+1, cmap.atom2.idx+1, + cmap.atom3.idx+1, cmap.atom4.idx+1, + cmap.atom2.idx+1, cmap.atom3.idx+1, + cmap.atom4.idx+1, cmap.atom5.idx+1) + ) + dest.write('\n') + dest.write('\n') + # Done! + # If we opened our own handle, close it + if own_handle: + dest.close() diff --git a/pmx/workflow/parmed/formats/registry.py b/pmx/workflow/parmed/formats/registry.py new file mode 100644 index 00000000..881c30ee --- /dev/null +++ b/pmx/workflow/parmed/formats/registry.py @@ -0,0 +1,217 @@ +""" +This module contains the metaclass for defining and registering a particular +file format. Any class with this metaclass will be added to the registry and +therefore automatically be added to the 'automatic' file type identification. + +The following static class functions will trigger special behavior: + + - id_format(file) : Takes a filename to identify the type, and return True + if the file is that format or False if not. + + - parse(file) : Takes a file name or file-like object, parse through the + whole thing and return it. If this method is not found, the constructor is + called directly. + +Note, id_format must be IMPLEMENTED for each class added to the registry, not +simply inherited from a base class (unless that base class is not a metaclass of +FileFormatType) +""" +from contextlib import closing +from ..utils.io import genopen +from ..exceptions import FormatNotFound +import os + +PARSER_REGISTRY = dict() +PARSER_ARGUMENTS = dict() + +class FileFormatType(type): + """ + Metaclass for registering parsers for different formats of different types + of files. + + Parameters + ---------- + cls : class type + The class that is being generated by this metaclass + name : str + The name of the class being created + bases : tuple of types + Tuple of all base class types for this class + dct : dict + The list of options and attributes currently present in the class + """ + def __init__(cls, name, bases, dct): + global PARSER_REGISTRY, _CLASS_REGISTRY + if name in PARSER_REGISTRY: + raise ValueError('Duplicate name %s in parser registry' % name) + if 'id_format' in dct: + PARSER_REGISTRY[name] = cls + if 'extra_args' in dct: + PARSER_ARGUMENTS[name] = dct['extra_args'] + else: + PARSER_ARGUMENTS[name] = () + super(FileFormatType, cls).__init__(name, bases, dct) + +def load_file(filename, *args, **kwargs): + """ + Identifies the file format of the specified file and returns its parsed + contents. + + Parameters + ---------- + filename : str + The name of the file to try to parse. If the filename starts with + http:// or https:// or ftp://, it is treated like a URL and the file will be + loaded directly from its remote location on the web + structure : object, optional + For some classes, such as the Mol2 file class, the default return object + is not a Structure, but can be made to return a Structure if the + ``structure=True`` keyword argument is passed. To facilitate writing + easy code, the ``structure`` keyword is always processed and only passed + on to the correct file parser if that parser accepts the structure + keyword. There is no default, as each parser has its own default. + natom : int, optional + This is needed for some coordinate file classes, but not others. This is + treated the same as ``structure``, above. It is the # of atoms expected + hasbox : bool, optional + Same as ``structure``, but indicates whether the coordinate file has + unit cell dimensions + skip_bonds : bool, optional + Same as ``structure``, but indicates whether or not bond searching will + be skipped if the topology file format does not contain bond information + (like PDB, GRO, and PQR files). + *args : other positional arguments + Some formats accept positional arguments. These will be passed along + **kwargs : other options + Some formats can only be instantiated with other options besides just a + file name. + + Returns + ------- + object + The returned object is the result of the parsing function of the class + associated with the file format being parsed + + Notes + ----- + Compressed files are supported and detected by filename extension. This + applies both to local and remote files. The following names are supported: + + - ``.gz`` : gzip compressed file + - ``.bz2`` : bzip2 compressed file + + SDF file is loaded via `rdkit` package. + + Examples + -------- + + Load a Mol2 file + + >>> load_file('tripos1.mol2') + + + Load a Mol2 file as a Structure + + >>> load_file('tripos1.mol2', structure=True) + + + Load an Amber topology file + + >>> load_file('trx.prmtop', xyz='trx.inpcrd') + + + Load a CHARMM PSF file + + >>> load_file('ala_ala_ala.psf') + + + Load a PDB and CIF file + + >>> load_file('4lzt.pdb') + + >>> load_file('4LZT.cif') + + + Load a Gromacs topology file -- only works with Gromacs installed + + >>> load_file('1aki.ff99sbildn.top') + + + Load a SDF file -- only works with rdkit installed + + >>> load_file('mol.sdf', structure=True) + + + Raises + ------ + FileNotFoundError + If ``filename`` does not exist + + parmed.exceptions.FormatNotFound + If no suitable file format can be identified, a TypeError is raised + + TypeError + If the identified format requires additional arguments that are not + provided as keyword arguments in addition to the file name + """ + global PARSER_REGISTRY, PARSER_ARGUMENTS + + # Check that the file actually exists and that we can read it + if filename.startswith('http://') or filename.startswith('https://')\ + or filename.startswith('ftp://'): + # This raises IOError if it does not exist; assert silences linters + with closing(genopen(filename)) as f: + assert f + elif not os.path.exists(filename): + raise FileNotFoundError('%s does not exist' % filename) + elif not os.access(filename, os.R_OK): + raise FileNotFoundError('%s does not have read permissions set' % filename) + + for name, cls in PARSER_REGISTRY.items(): + if not hasattr(cls, 'id_format'): + continue + try: + if cls.id_format(filename): + break + except (UnicodeDecodeError, SyntaxError): + continue + else: + # We found no file format + raise FormatNotFound('Could not identify file format') + + # We found a file format that is compatible. Parse it! + other_args = PARSER_ARGUMENTS[name] + for arg in other_args: + if not arg in kwargs: + raise TypeError('%s constructor expects %s keyword argument' % name, arg) + # Pass on the following keywords IFF the target function accepts a target + # keyword. Otherwise, get rid of it: structure, natom, hasbox, skip_bonds + if hasattr(cls, 'parse'): + _prune_argument(cls.parse, kwargs, 'structure') + _prune_argument(cls.parse, kwargs, 'natom') + _prune_argument(cls.parse, kwargs, 'hasbox') + _prune_argument(cls.parse, kwargs, 'skip_bonds') + return cls.parse(filename, *args, **kwargs) + elif hasattr(cls, 'open_old'): + _prune_argument(cls.open_old, kwargs, 'structure') + _prune_argument(cls.open_old, kwargs, 'natom') + _prune_argument(cls.open_old, kwargs, 'hasbox') + _prune_argument(cls.open_old, kwargs, 'skip_bonds') + return cls.open_old(filename, *args, **kwargs) + elif hasattr(cls, 'open'): + _prune_argument(cls.open, kwargs, 'structure') + _prune_argument(cls.open, kwargs, 'natom') + _prune_argument(cls.open, kwargs, 'hasbox') + _prune_argument(cls.open, kwargs, 'skip_bonds') + return cls.open(filename, *args, **kwargs) + _prune_argument(cls.__init__, kwargs, 'structure') + _prune_argument(cls.__init__, kwargs, 'natom') + _prune_argument(cls.__init__, kwargs, 'hasbox') + _prune_argument(cls.__init__, kwargs, 'skip_bonds') + return cls(filename, *args, **kwargs) + +def _prune_argument(func, kwargs, keyword): + if keyword in kwargs: + if (keyword not in + func.__code__.co_varnames[:func.__code__.co_argcount]): + kwargs.pop(keyword) diff --git a/pmx/workflow/parmed/formats/sdf.py b/pmx/workflow/parmed/formats/sdf.py new file mode 100644 index 00000000..67de2274 --- /dev/null +++ b/pmx/workflow/parmed/formats/sdf.py @@ -0,0 +1,32 @@ +""" +This module contains parsers for sdf file format +extension described at https://www.cas.org/content/chemical-suppliers/example-sdf +""" +import linecache + +from .registry import FileFormatType +from .. import rdkit + +class SDFFile(metaclass=FileFormatType): + """ Class to read SDF file """ + + @staticmethod + def id_format(filename): + """ Identify the file as a SDF file format or not + + Parameters + ---------- + filename : str + Name of the file to test whether or not it is a sdf file + + Returns + ------- + is_fmt : bool + True if it is a sdf file, False otherwise + """ + words = linecache.getline(filename, lineno=4).split() + return len(words) >= 3 and words[-1] in ('V2000', 'V3000') + + @staticmethod + def parse(filename, structure=False): + return rdkit.from_sdf(filename, structure=structure) diff --git a/pmx/workflow/parmed/geometry.py b/pmx/workflow/parmed/geometry.py new file mode 100644 index 00000000..00c68c4d --- /dev/null +++ b/pmx/workflow/parmed/geometry.py @@ -0,0 +1,370 @@ +""" +This module contains the functionality for carrying out geometrical calculations +for molecules and molecular systems + +Author: Jason Swails +Contributors: + +Copyright (C) 2014 - 2015 Jason Swails + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU Lesser General Public License as published by the Free +Software Foundation; either version 2 of the License, or (at your option) any +later version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +59 Temple Place - Suite 330 +Boston, MA 02111-1307, USA. +""" +import warnings +from collections import defaultdict +from math import pi, cos, sin, sqrt, acos + +import numpy as np + +from . import unit as u +from .constants import TINY, DEG_TO_RAD, RAD_TO_DEG +from .vec3 import Vec3 + +def box_lengths_and_angles_to_vectors(a, b, c, alpha, beta, gamma): + """ + This function takes the lengths of the unit cell vectors and the angles + between them and returns 3 unit cell vectors satisfying those dimensions + + Parameters + ---------- + a : double (or length Quantity) + Length of the first unit cell vector + b : double (or length Quantity) + Length of the second unit cell vector + c : double (or length Quantity) + Length of the third unit cell vector + alpha : double (or angle Quantity) + Angle between vectors b and c + beta : double (or angle Quantity) + Angle between vectors a and c + gamma : double (or angle Quantity) + Angle between vectors a and b + + Returns + ------- + list Quantity, list Quantity, list Quantity + The 3, 3-element vectors as quantities with dimension length + + Notes + ----- + The unit cell lengths are assumed to be Angstroms if no explicit unit is + given. The angles are assumed to be degrees + """ + if u.is_quantity(a): a = a.value_in_unit(u.angstroms) + if u.is_quantity(b): b = b.value_in_unit(u.angstroms) + if u.is_quantity(c): c = c.value_in_unit(u.angstroms) + if u.is_quantity(alpha): alpha = alpha.value_in_unit(u.degrees) + if u.is_quantity(beta): beta = beta.value_in_unit(u.degrees) + if u.is_quantity(gamma): gamma = gamma.value_in_unit(u.degrees) + + if alpha <= 2*pi and beta <= 2*pi and gamma <= 2*pi: + warnings.warn('Strange unit cell vector angles detected. They ' + 'appear to be in radians...') + + alpha *= DEG_TO_RAD + beta *= DEG_TO_RAD + gamma *= DEG_TO_RAD + + av = [a, 0.0, 0.0] + bx = b * cos(gamma) + by = b * sin(gamma) + bv = [bx, by, 0.0] + cx = c * cos(beta) + cy = c * (cos(alpha) - cos(beta)*cos(gamma)) / sin(gamma) + cz = sqrt(c*c - cx*cx - cy*cy) + cv = [cx, cy, cz] + + # Make sure that any tiny components are exactly 0 + if abs(bx) < TINY: bv[0] = 0 + if abs(by) < TINY: bv[1] = 0 + if abs(cx) < TINY: cv[0] = 0 + if abs(cy) < TINY: cv[1] = 0 + if abs(cz) < TINY: cv[2] = 0 + + return (av, bv, cv) * u.angstroms + +def box_vectors_to_lengths_and_angles(a, b, c): + """ + This function takes the lengths of the unit cell vectors and the angles + between them and returns 3 unit cell vectors satisfying those dimensions + + Parameters + ---------- + a : collection of 3 floats (or length Quantity) + The first unit cell vector + b : collection of 3 floats (or length Quantity) + The second unit cell vector + c : collection of 3 floats (or length Quantity) + The third unit cell vector + + Returns + ------- + (a, b, c), (alpha, beta, gamma) + Two tuples, the first is the 3 unit cell vector lengths as + length-dimension Quantity objects and the second is the set of angles + between the unit cell vectors as angle-dimension Quantity objects + + Notes + ----- + The unit cell lengths are assumed to be Angstroms if no explicit unit is + given. + """ + if u.is_quantity(a): a = a.value_in_unit(u.angstroms) + if u.is_quantity(b): b = b.value_in_unit(u.angstroms) + if u.is_quantity(c): c = c.value_in_unit(u.angstroms) + # Get the lengths + la = sqrt(a[0]*a[0] + a[1]*a[1] + a[2]*a[2]) + lb = sqrt(b[0]*b[0] + b[1]*b[1] + b[2]*b[2]) + lc = sqrt(c[0]*c[0] + c[1]*c[1] + c[2]*c[2]) + # Angles + alpha = acos((b[0]*c[0] + b[1]*c[1] + b[2]*c[2]) / (lb*lc)) + beta = acos((a[0]*c[0] + a[1]*c[1] + a[2]*c[2]) / (la*lc)) + gamma = acos((b[0]*a[0] + b[1]*a[1] + b[2]*a[2]) / (lb*la)) + # Convert to degrees + alpha *= RAD_TO_DEG + beta *= RAD_TO_DEG + gamma *= RAD_TO_DEG + + return (la, lb, lc) * u.angstroms, (alpha, beta, gamma) * u.degrees + +def reduce_box_vectors(a, b, c): + """ + This function puts three unit cell vectors in a reduced form where a is + "mostly" in x, b is "mostly" in y, and c is "mostly" in z. This form is + necessary for some programs (notably OpenMM and Gromacs) + + Parameters + ---------- + a : 3-element collection of float + First unit cell vector + b : 3-element collection of float + Second unit cell vector + c : 3-element collection of float + Third unit cell vector + + Returns + ------- + red_a, red_b, red_c : Vec3, Vec3, Vec3 + The reduced unit cell vectors in units of angstroms + + Notes + ----- + The implementation here is taken from the OpenMM Python application layer + written by Peter Eastman + """ + if u.is_quantity(a): + a = a.value_in_unit(u.angstroms) + if u.is_quantity(b): + b = b.value_in_unit(u.angstroms) + if u.is_quantity(c): + c = c.value_in_unit(u.angstroms) + + a = Vec3(*a) + b = Vec3(*b) + c = Vec3(*c) + + c = c - b*round(c[1]/b[1]) + c = c - a*round(c[0]/a[0]) + b = b - a*round(b[0]/a[0]) + + return a, b, c + + +def center_of_mass(coordinates, masses): + """ Compute the center of mass of a group of coordinates. + + Parameters + ---------- + coordinates : numpy.ndarray + Coordinate array + masses : numpy.ndarray + Array of masses + + Returns + ------- + COM + np.ndarray of shape (3,) identifying the cartesian center of mass + + Notes + ----- + This method *requires* that the parameters be passed in as numpy arrays. + AttributeError's will ensue if this is not the case. Also, coordinates must + be able to be reshaped to (len(masses), 3), or ValueError's will ensue + """ + masses = masses.flatten() + coordinates = coordinates.reshape((masses.shape[0], 3)) + return np.average(coordinates, weights=masses, axis=0) + +def distance2(a1, a2): + """ Computes the cartesian distance between two atoms. Ignores periodic boundary conditions. + + Parameters + ---------- + a1, a2 : Atom or collection of 3 coordinates + The two atoms between whom the distance should be calculated + + Returns + ------- + d2 : float + The square of the distance between the two atoms + + Notes + ----- + This is done in pure Python, so it should not be used for large numbers of + distance calculations. For that, use numpy-vectorized routines and the numpy + coordinate arrays + + Raises + ------ + TypeError if a1 or a2 are not Atom or iterable + ValueError if a1 or a2 are iterable, but do not have exactly 3 items + """ + x1, y1, z1 = _get_coords_from_atom_or_tuple(a1) + x2, y2, z2 = _get_coords_from_atom_or_tuple(a2) + dx = x1 - x2 + dy = y1 - y2 + dz = z1 - z2 + return dx*dx + dy*dy + dz*dz + +def angle(a1, a2, a3): + """ Computes the cartesian angle between three atoms. Ignores periodic boundary conditions. + + Parameters + ---------- + a1, a2, a3 : Atom or collection of 3 coordinates + The three atoms between whom the angle should be calculated (with a2 + being the central atoms) + + Returns + ------- + ang : float + The angle between the vectors a1-a2 and a2-a3 in degrees + + Notes + ----- + This is done in pure Python, so it should not be used for large numbers of + distance calculations. For that, use numpy-vectorized routines and the numpy + coordinate arrays + + Raises + ------ + TypeError if a1, a2, or a3 are not Atom or iterable + ValueError if a1, a2, or a3 are iterable, but do not have exactly 3 items + """ + x1, y1, z1 = _get_coords_from_atom_or_tuple(a1) + x2, y2, z2 = _get_coords_from_atom_or_tuple(a2) + x3, y3, z3 = _get_coords_from_atom_or_tuple(a3) + v1 = np.array([x2 - x1, y2 - y1, z2 - z1]) + v2 = np.array([x2 - x3, y2 - y3, z2 - z3]) + l1 = np.sqrt(np.dot(v1, v1)) + l2 = np.sqrt(np.dot(v2, v2)) + cosa = np.dot(v1, v2) / (l1 * l2) + return np.degrees(np.arccos(cosa)) + +def dihedral(a1, a2, a3, a4): + """ + Computes the angle between three vectors made up of four points (all three + vectors share one point with one other vector) + + Parameters + ---------- + a1, a2, a3, a4 : Atom or collection of 4 coordinates + The four atoms between whom the torsion angle should be calculated (with + a1 and a4 being the two end-point atoms not shared between two vectors) + + Returns + ------- + dihed : float + The measured dihedral between the 4 points in degrees + """ + p = np.array([_get_coords_from_atom_or_tuple(a1), + _get_coords_from_atom_or_tuple(a2), + _get_coords_from_atom_or_tuple(a3), + _get_coords_from_atom_or_tuple(a4)]) + v1 = p[1] - p[0] + v2 = p[1] - p[2] + v3 = p[3] - p[2] + # Take the cross product between v1-v2 and v2-v3 + v1xv2 = _cross(v1, v2) + v2xv3 = _cross(v2, v3) + # Now find the angle between these cross-products + l1 = np.sqrt(np.dot(v1xv2, v1xv2)) + l2 = np.sqrt(np.dot(v2xv3, v2xv3)) + cosa = np.dot(v1xv2, v2xv3) / (l1 * l2) + if np.dot(v3, v1xv2) <= 0.0 : + return np.degrees(np.arccos(cosa)) + else : + return -np.degrees(np.arccos(cosa)) + +def _cross(v1, v2): + """ Computes the cross-product """ + # Can't use np.cross for pypy, since it's not yet implemented + return np.array([v1[1]*v2[2] - v1[2]*v2[1], + v1[2]*v2[0] - v1[0]*v2[2], + v1[0]*v2[1] - v1[1]*v2[0]]) + + +def _get_coords_from_atom_or_tuple(a): + from .topologyobjects import Atom + if isinstance(a, Atom): + return a.xx, a.xy, a.xz + return a + +# tuples are pairs of atomic numbers followed by the distance cutoff below which +# they are considered "bonded". This is taken from Atom.cpp in cpptraj +# (Atom::GetBondLength) +_OFFSET = 0.20 # offset for what is considered a bond +_DEFAULT_CUTOFF = (1.6 + _OFFSET)**2 +STANDARD_BOND_LENGTHS_SQUARED = defaultdict(lambda: _DEFAULT_CUTOFF) +STANDARD_BOND_LENGTHS_SQUARED.update({ + # Self-bonds + (1, 1) : (0.74 + _OFFSET)**2, + (6, 6) : (1.54 + _OFFSET)**2, + (7, 7) : (1.45 + _OFFSET)**2, + (8, 8) : (1.48 + _OFFSET)**2, + (15, 15) : (2.21 + _OFFSET)**2, + (16, 16) : (2.05 + _OFFSET)**2, + # H- + (1, 6) : (1.09 + _OFFSET)**2, (6, 1) : (1.09 + _OFFSET)**2, + (1, 7) : (1.01 + _OFFSET)**2, (7, 1) : (1.01 + _OFFSET)**2, + (1, 8) : (0.96 + _OFFSET)**2, (8, 1) : (0.96 + _OFFSET)**2, + (1, 15) : (1.44 + _OFFSET)**2, (15, 1) : (1.44 + _OFFSET)**2, + (1, 16) : (1.34 + _OFFSET)**2, (16, 1) : (1.34 + _OFFSET)**2, + # C- + (6, 7) : (1.47 + _OFFSET)**2, (7, 6) : (1.47 + _OFFSET)**2, + (6, 8) : (1.43 + _OFFSET)**2, (8, 6) : (1.43 + _OFFSET)**2, + (6, 9) : (1.35 + _OFFSET)**2, (9, 6) : (1.35 + _OFFSET)**2, + (6, 15) : (1.84 + _OFFSET)**2, (15, 6) : (1.84 + _OFFSET)**2, + (6, 16) : (1.82 + _OFFSET)**2, (16, 6) : (1.82 + _OFFSET)**2, + (6, 17) : (1.77 + _OFFSET)**2, (17, 6) : (1.77 + _OFFSET)**2, + (6, 35) : (1.94 + _OFFSET)**2, (35, 6) : (1.94 + _OFFSET)**2, + # N- + (7, 8) : (1.40 + _OFFSET)**2, (8, 7) : (1.40 + _OFFSET)**2, + (7, 9) : (1.36 + _OFFSET)**2, (9, 7) : (1.36 + _OFFSET)**2, + (7, 15) : (1.71 + _OFFSET)**2, (15, 7) : (1.71 + _OFFSET)**2, + (7, 16) : (1.68 + _OFFSET)**2, (16, 7) : (1.68 + _OFFSET)**2, + (7, 17) : (1.75 + _OFFSET)**2, (17, 7) : (1.75 + _OFFSET)**2, + # O- + (8, 9) : (1.42 + _OFFSET)**2, (9, 8) : (1.42 + _OFFSET)**2, + (8, 15) : (1.63 + _OFFSET)**2, (15, 8) : (1.63 + _OFFSET)**2, + (8, 16) : (1.48 + _OFFSET)**2, (16, 8) : (1.48 + _OFFSET)**2, + # F- + (9, 15) : (1.54 + _OFFSET)**2, (15, 9) : (1.54 + _OFFSET)**2, + (9, 16) : (1.56 + _OFFSET)**2, (16, 9) : (1.56 + _OFFSET)**2, + # P- + (15, 16) : (1.86 + _OFFSET)**2, (16, 15) : (1.86 + _OFFSET)**2, + (15, 17) : (2.03 + _OFFSET)**2, (17, 15) : (2.03 + _OFFSET)**2, + # S- + (16, 17) : (2.07 + _OFFSET)**2, (17, 16) : (2.07 + _OFFSET)**2, +}) diff --git a/pmx/workflow/parmed/gromacs/__init__.py b/pmx/workflow/parmed/gromacs/__init__.py new file mode 100644 index 00000000..0c080080 --- /dev/null +++ b/pmx/workflow/parmed/gromacs/__init__.py @@ -0,0 +1,45 @@ +""" +Contains classes for parsing GROMACS topology and parameter files +""" +import os as _os +from ..utils import which as _which + +__all__ = ['GROMACS_TOPDIR', 'GromacsTopologyFile', 'GromacsGroFile'] + +GROMACS_TOPDIR = None + +if _os.getenv('GMXDATA') is not None and _os.path.isdir( + _os.path.join(_os.getenv('GMXDATA'), 'top')): + GROMACS_TOPDIR = _os.path.join(_os.getenv('GMXDATA'), 'top') +elif _os.getenv('GMXBIN') is not None and _os.path.isdir( + _os.path.join(_os.getenv('GMXBIN'), '..', 'share', 'gromacs', 'top')): + GROMACS_TOPDIR = _os.path.join(_os.getenv('GMXBIN'), '..', 'share', 'gromacs', 'top') +else: + for _testdir in ['/usr', '/usr/local', '/opt/local', '/opt']: + if _os.path.isdir(_os.path.join(_testdir, 'share', 'gromacs')): + GROMACS_TOPDIR = _os.path.join(_testdir, 'share', 'gromacs', 'top') + break + +if GROMACS_TOPDIR is None: + if _which('gmx') is not None: + GROMACS_TOPDIR = _os.path.join(_os.path.split(_which('gmx'))[0], + '..', 'share', 'gromacs', 'top') + elif _which('pdb2gmx') is not None: + GROMACS_TOPDIR = _os.path.join(_os.path.split(_which('pdb2gmx'))[0], + '..', 'share', 'gromacs', 'top') + +if GROMACS_TOPDIR is not None: + # Regularize the include path + GROMACS_TOPDIR = _os.path.realpath(GROMACS_TOPDIR) +else: + # Use the default Gromacs installation path + GROMACS_TOPDIR = '/usr/local/gromacs/share/gromacs/top' + +try: + del _testdir +except NameError: + pass +del _os, _which + +from .gromacstop import GromacsTopologyFile +from .gromacsgro import GromacsGroFile diff --git a/pmx/workflow/parmed/gromacs/_cpp.py b/pmx/workflow/parmed/gromacs/_cpp.py new file mode 100644 index 00000000..f9e0b00a --- /dev/null +++ b/pmx/workflow/parmed/gromacs/_cpp.py @@ -0,0 +1,379 @@ +""" +A little utility for performing C-like preprocessing using some standard CPP +directives like #if, #ifdef, and #define. + +Written by Jason Swails +""" +import re +import warnings +from functools import wraps +from collections import OrderedDict +from os import path +from ..exceptions import PreProcessorError, PreProcessorWarning +from ..utils.io import genopen + +ppre = re.compile(r'#\s*(ifdef|ifndef|if|else|elif|endif|define|undef|include)\s*(.+)?') +ppcomments = re.compile(r'(?://.+|/\*(?:.*)\*/)') +includere = re.compile(r'[<"](.+)[>"]') +novarcharre = re.compile(r'\W') + +def _strip_pp_comments(func): + """ + Decorator to apply to functions that will strip out C-style comments before + calling the preprocessor function on the resulting arguments to the + preprocessor directive + """ + @wraps(func) + def wrapper(self, args): + args = ppcomments.sub('', args) + return func(self, args) + return wrapper + +def _find_all_instances_in_string(string, substr): + """ Find indices of all instances of substr in string """ + indices = [] + idx = string.find(substr, 0) + while idx > -1: + indices.append(idx) + idx = string.find(substr, idx+1) + return indices + +def _replace_defines(line, defines): + """ Replaces defined tokens in a given line """ + if not defines: return line + for define in reversed(defines): + value = defines[define] + indices = _find_all_instances_in_string(line, define) + if not indices: continue + # Check to see if it's inside of quotes + inside = '' + idx = 0 + n_to_skip = 0 + new_line = [] + for i, char in enumerate(line): + if n_to_skip: + n_to_skip -= 1 + continue + if char in ('\'"'): + if not inside: + inside = char + else: + if inside == char: + inside = '' + if idx < len(indices) and i == indices[idx]: + if inside: + new_line.append(char) + idx += 1 + continue + if i == 0 or novarcharre.match(line[i-1]): + endidx = indices[idx] + len(define) + if endidx >= len(line) or novarcharre.match(line[endidx]): + new_line.extend(list(value)) + n_to_skip = len(define) - 1 + idx += 1 + continue + idx += 1 + new_line.append(char) + line = ''.join(new_line) + + return line + +# To track where in the "if-elif-else" block each conditional is +_IN_IF = 'in if' +_IN_ELIF = 'in elif' +_IN_ELSE = 'in else' + +class CPreProcessor(object): + """ + Steps through a file line-by-line, yielding only the preprocessed contents + + Parameters + ---------- + fname : str or file-like + The file name of the file to open or the open file object + defines : dict{str : value}, optional + The dict of defines to apply to this preprocessed file if any; + equivalent to the list of -DXYZ=ABC in the standard preprocessor + includes : list of str, optional + List of directory names to search for included files, if any + notfound_fatal : bool, optional + If True, include files not found are fatal. If False, they will simply + be skipped (with a warning emitted). Default True + + Notes + ----- + If ``fname`` is a file name, the directory containing that file name is the + first directory searched for include files. If ``fname`` is a file-like + object, then the current directory is the first searched. + """ + + def __init__(self, fname, defines=None, includes=None, notfound_fatal=True): + if isinstance(fname, str): + self._fileobj = genopen(fname, 'r') + self._ownhandle = True + curpath = path.abspath(path.split(fname)[0]) + self.filename = fname + else: + self._fileobj = fname + self._ownhandle = False + curpath = path.abspath(path.curdir) + self.filename = None + if includes is None: + self._includes = [curpath] + else: + self._includes = [curpath] + list(includes) + if defines is None: + self.defines = OrderedDict() + else: + # Convert every define to a string + self.defines = OrderedDict() + for define, value in defines.items(): + self.defines[define] = str(value) + self._notfound_fatal = notfound_fatal + + # Now to keep track of other basic logic stuff + self.included_files = [] + self._ifstack = [] + self._elsestack = [] + self._satisfiedstack = [] + self._num_ignoring_if = 0 + self._includefile = None + + def __del__(self): + self.close() + + def close(self): + if hasattr(self, '_ownhandle') and self._ownhandle: + self._fileobj.close() + + def readline(self): + try: + return next(iter(self)) + except StopIteration: + return '' + + def readlines(self): + return [line for line in self] + + def read(self): + return ''.join(self.readlines()) + + def tell(self): + return self._fileobj.tell() + + def seek(self, value): + raise NotImplementedError('Cannot seek through a preprocessed file') + + def __iter__(self): + """ Step through the preprocessed file line-by-line """ + for line in self._fileobj: + rematch = ppre.match(line) + if rematch: + cmd, args = rematch.groups() + args = args or '' + self._ppcmdmap[cmd](self, args) + # If we defined an include file, step through it + if self._includefile is not None: + for line in self._includefile: + yield line + self._includefile.close() + # We have to pass our defines back to our caller + self.defines = self._includefile.defines + self._includefile = None + continue + + if self._satisfiedstack and not self._satisfiedstack[-1]: + # We are inside an unsatisfied conditional + continue + + yield _replace_defines(line, self.defines) + # Make sure we don't have any dangling ifs + if self._ifstack: + raise PreProcessorError('EOF: Unterminated #if(def)') + + @_strip_pp_comments + def _pp_if(self, args): + if self._satisfiedstack and not self._satisfiedstack[-1]: + self._num_ignoring_if += 1 + return + args = args.strip() + if args.strip() in ('0', '1'): + satisfied = bool(int(args)) + self._ifstack.append(str(bool(satisfied))) + self._satisfiedstack.append(bool(satisfied)) + self._elsestack.append(_IN_IF) + return + raise NotImplementedError('Only "#if 0|1" is currently supported') + + @_strip_pp_comments + def _pp_ifdef(self, args): + if self._satisfiedstack and not self._satisfiedstack[-1]: + self._num_ignoring_if += 1 + return + words = args.split() + if len(words) == 0: + raise PreProcessorError(f'Bad #ifdef syntax: "#ifdef {args}"') + elif len(words) > 1: + warnings.warn("Ignored tokens in #ifdef: {', '.join(words[1:])}", PreProcessorWarning) + self._ifstack.append(f'{words[0]} in self.defines') + self._elsestack.append(_IN_IF) + self._satisfiedstack.append(words[0] in self.defines) + + @_strip_pp_comments + def _pp_ifndef(self, args): + if self._satisfiedstack and not self._satisfiedstack[-1]: + self._num_ignoring_if += 1 + return + words = args.split() + if len(words) == 0: + raise PreProcessorError(f'Bad #ifndef syntax: "#ifndef {args}"') + elif len(words) > 1: + warnings.warn(f"Ignored tokens in #ifndef: {', '.join(words[1:])}", PreProcessorWarning) + self._ifstack.append(f'{words[0]} not in self.defines') + self._elsestack.append(_IN_IF) + self._satisfiedstack.append(words[0] not in self.defines) + + @_strip_pp_comments + def _pp_elif(self, args): + raise NotImplementedError('#elif conditionals are not yet implemented.') + + @_strip_pp_comments + def _pp_else(self, args): + if self._num_ignoring_if > 0: + return + if not self._ifstack: + raise PreProcessorError('#else missing #if(def)') + words = args.split() + if len(words) > 0: + warnings.warn(f"Ignored tokens in #else: {', '.join(words[1:])}", PreProcessorWarning) + if self._elsestack[-1] == _IN_ELSE: + raise PreProcessorError('#else following #else') + self._elsestack[-1] = _IN_ELSE + self._satisfiedstack[-1] = not self._satisfiedstack[-1] + + @_strip_pp_comments + def _pp_endif(self, args): + if self._num_ignoring_if > 0: + self._num_ignoring_if -= 1 + return + if args.strip(): + warnings.warn(f'Ignored tokens in #endif: {args.strip()}', PreProcessorWarning) + if not self._ifstack: + raise PreProcessorError('#endif missing #if(def)') + self._ifstack.pop() + self._satisfiedstack.pop() + self._elsestack.pop() + + @_strip_pp_comments + def _pp_include(self, args): + if self._satisfiedstack and not self._satisfiedstack[-1]: + return + # Locate the include file + rematch = includere.match(args) + if not rematch: + raise PreProcessorError('Bad #include syntax') + includefile = rematch.groups()[0] + self.included_files.append(includefile) + for folder in self._includes: + testfile = path.join(folder, includefile) + if path.isfile(testfile): + break + else: + if self._notfound_fatal: + raise PreProcessorError(f'Could not find {includefile}') + warnings.warn(f'Could not find {includefile}; skipping', PreProcessorWarning) + return + self._includefile = CPreProcessor(testfile, + defines=self.defines, + includes=self._includes, + notfound_fatal=self._notfound_fatal) + + @_strip_pp_comments + def _pp_define(self, args): + if self._satisfiedstack and not self._satisfiedstack[-1]: + return + # Define a new variable + words = args.split() + if len(words) == 0: + raise PreProcessorError('Nothing defined in #define') + # Warn about a double-define + if words[0] in self.defines: + warnings.warn(f'{words[0]} already defined; overwriting', PreProcessorWarning) + if len(words) == 1: + self.defines[words[0]] = '1' + elif len(words) >= 2: + self.defines[words[0]] = args[len(words[0]):].strip() + + @_strip_pp_comments + def _pp_undef(self, args): + if self._satisfiedstack and not self._satisfiedstack[-1]: + return + # Define a new variable + words = args.split() + if len(words) == 1: + try: + del self.defines[words[0]] + except KeyError: + # Undefining an undefined variable is a no-op + pass + elif len(words) > 1: + warnings.warn(f"Ignored tokens in #undef: {', '.join(words[1:])}", PreProcessorWarning) + elif len(words) == 0: + raise PreProcessorError('Nothing defined in #undef') + + # Context manager protocol + def __exit__(self, type, value, traceback): + self.close() + + def __enter__(self): + return self + + _ppcmdmap = {'if' : _pp_if, 'elif' : _pp_elif, 'ifdef' : _pp_ifdef, + 'else' : _pp_else, 'define' : _pp_define, 'undef' : _pp_undef, + 'include' : _pp_include, 'endif' : _pp_endif, + 'ifndef' : _pp_ifndef} + +if __name__ == '__main__': + # Act as a stand-alone preprocessor + import argparse + import sys + parser = argparse.ArgumentParser() + parser.add_argument('-i', '--input-file', dest='input', metavar='FILE', + required=True, help='''Input file to pre-process. Either a file + name or, if '-' is given, from standard input.''') + parser.add_argument('-o', '--output-file', dest='output', metavar='FILE', + default=None, help='''Output file with preprocessed results. + Default is standard output''') + parser.add_argument('-D', dest='defines', metavar='VAR[=VAL]', + action='append', help='''List of predefined variables to pass to + the preprocessor. Default VAL is 1 when missing.''', default=[]) + parser.add_argument('-I', dest='includes', metavar='DIRECTORY', + action='append', help='''List of include directories to search + for included files''', default=[]) + + opt = parser.parse_args() + + defines = OrderedDict() + for define in opt.defines: + if '=' in define: + define, val = define.split('=') + else: + val = '1' + defines[define] = val + + if opt.input == '-': + f = sys.stdin + else: + f = opt.input + pp = CPreProcessor(f, defines=defines, includes=opt.includes) + if opt.output is None: + output = sys.stdout + own_handle = False + else: + output = genopen(opt.output, 'w') + own_handle = True + + for line in pp: + output.write(line) + if own_handle: + output.close() diff --git a/pmx/workflow/parmed/gromacs/_gromacsfile.py b/pmx/workflow/parmed/gromacs/_gromacsfile.py new file mode 100644 index 00000000..c6d7bedc --- /dev/null +++ b/pmx/workflow/parmed/gromacs/_gromacsfile.py @@ -0,0 +1,108 @@ +""" +Provides a class for reading GROMACS-style files. The key component to these +files is that the ; character is a comment character and everything after ; is +ignored. +""" +from ._cpp import CPreProcessor + +class GromacsFile: + """ + A GROMACS file that recognizes the ";" character as a 'comment' token. It + can be iterated over and generally treated like a file object, but only + spits out strings that have been truncated at its first comment character. + + There is currently no way to recognize a ; as a _non_ comment character, + since allowing an escape character does not seem to be common practice and + would likely introduce negative performance implications. + + Parameters + ---------- + fname : str or file-like + Name of the file to parse or file-like object to parse + defines : dict{str : str}, optional + List of defines for the preprocessed file, if any + includes : list of str, optional + List of include files. Default is taken from environment variables + GMXDATA or GMXBIN if they are set. Otherwise, it is looked for in /usr, + /usr/local, /opt, or /opt/local. If it is still not found, it is looked + for relative to whatever ``mdrun`` executable is in your path + notfound_fatal : bool, optional + If True, missing include files are fatal. If False, they are a warning. + Default is True + """ + + def __init__(self, fname, **kwargs): + self._handle = CPreProcessor(fname, **kwargs) + self.closed = False + self.line_number = 0 + + def __iter__(self): + # Iterate over the file, treating an ending \ as a continuation + parts = [] + for line in self._handle: + try: + idx = line.index(';') + if not parts: + yield '%s\n' % line[:idx] + else: + parts.append('%s' % line[:idx]) + yield '%s\n' % ''.join(parts) + parts = [] + except ValueError: + # There is no comment... + if line.rstrip('\r\n').endswith('\\'): + chars = list(reversed(line.rstrip('\r\n'))) + del chars[chars.index('\\')] + parts.append('%s ' % ''.join(reversed(chars))) + elif parts: + parts.append(line) + yield ''.join(parts) + parts = [] + else: + yield line + + @property + def included_files(self): + return self._handle.included_files + + def readline(self): + parts = [] + self.line_number += 1 + line = True + while line: + line = self._handle.readline() + try: + idx = line.index(';') + if not parts: + return '%s\n' % line[:idx] + else: + parts.append('%s' % line[:idx]) + return '%s\n' % ''.join(parts) + except ValueError: + # There is no comment... + if line.rstrip('\r\n').endswith('\\'): + chars = list(reversed(line.rstrip('\r\n'))) + del chars[chars.index('\\')] + parts.append(f"{''.join(reversed(chars))} ") + elif parts: + parts.append(line) + return ''.join(parts) + else: + return line + + def readlines(self): + return [line for line in self] + + def read(self): + return ''.join(self.readlines()) + + def close(self): + self._handle.close() + self.closed = True + + def __del__(self): + try: + self.closed or self._handle.close() + except AttributeError: + # It didn't make it out of the constructor + pass diff --git a/pmx/workflow/parmed/gromacs/gromacsgro.py b/pmx/workflow/parmed/gromacs/gromacsgro.py new file mode 100644 index 00000000..1a0e9dc9 --- /dev/null +++ b/pmx/workflow/parmed/gromacs/gromacsgro.py @@ -0,0 +1,312 @@ +""" +This module contains functionality relevant to loading and parsing GROMACS GRO +(coordinate) files and building a stripped-down Structure from it +""" +from contextlib import closing +from ..constants import TINY +from ..exceptions import GromacsError +from ..formats.registry import FileFormatType +from ..geometry import ( + box_vectors_to_lengths_and_angles, box_lengths_and_angles_to_vectors, reduce_box_vectors +) +from ..periodic_table import AtomicNum, element_by_name, Mass +from ..structure import Structure +from ..topologyobjects import Atom, ExtraPoint +from .. import unit as u +from ..utils.io import genopen + +class _AtomLineParser: + """ Parses atom lines from GRO files """ + def __init__(self): + self._digits = None + self._pdeci = 0 + self._ndeci = 0 + + def read(self, line): + """ Reads a line + + Parameters + ---------- + line : str + A line with an atom record from a GRO file + + Returns + ------- + atom, resname, resnum : Atom, str, int + The Atom instance, residue name, and residue number containing the + atom + """ + resnum = int(line[:5]) + resname = line[5:10].strip() + atomname = line[10:15].strip() + elem = element_by_name(atomname) + atomic_number = AtomicNum[elem] + mass = Mass[elem] + atnum = int(line[15:20]) + if atomic_number == 0: + atom = ExtraPoint(name=atomname, number=atnum) + else: + atom = Atom(atomic_number=atomic_number, name=atomname, + number=atnum, mass=mass) + if self._digits is None: + self._pdeci = line.index('.', 20) + self._ndeci = line.index('.', self._pdeci+1) + self._digits = self._ndeci - self._pdeci + atom.xx, atom.xy, atom.xz = ( + float(line[20+i*self._digits:20+(i+1)*self._digits])*10 for i in range(3) + ) + wbeg = 20 + self._digits * 3 + wend = wbeg + self._digits + if line[wbeg:wend].strip(): + atom.vx, atom.vy, atom.vz = ( + float(line[wbeg+i*self._digits:wend+i*self._digits])*10 for i in range(3) + ) + return atom, resname, resnum + +class GromacsGroFile(metaclass=FileFormatType): + """ Parses and writes Gromacs GRO files """ + + @staticmethod + def id_format(filename): + """ Identifies the file as a GROMACS GRO file + + Parameters + ---------- + filename : str + Name of the file to check if it is a Gromacs GRO file + + Returns + ------- + is_fmt : bool + If it is identified as a Gromacs GRO file, return True. False + otherwise + """ + with closing(genopen(filename)) as f: + f.readline() # Title line + try: + int(f.readline().strip()) # number of atoms + except ValueError: + return False + line = f.readline() + try: + int(line[:5]) + if not line[5:10].strip(): + return False + if not line[10:15].strip(): + return False + int(line[15:20]) + pdeci = [i for i, x in enumerate(line) if x == '.'] + ndeci = pdeci[1] - pdeci[0] - 5 + for i in range(1, 4): + wbeg = (pdeci[0]-4)+(5+ndeci)*(i-1) + wend = (pdeci[0]-4)+(5+ndeci)*i + float(line[wbeg:wend]) + i = 4 + wbeg = (pdeci[0]-4)+(5+ndeci)*(i-1) + wend = (pdeci[0]-4)+(5+ndeci)*i + if line[wbeg:wend].strip(): + for i in range(4, 7): + wbeg = (pdeci[0]-4)+(5+ndeci)*(i-1) + wend = (pdeci[0]-4)+(5+ndeci)*i + float(line[wbeg:wend]) + except ValueError: + return False + return True + + @staticmethod + def parse(filename, skip_bonds=False): + """ Parses a Gromacs GRO file + + Parameters + ---------- + filename : str or file-like + Name of the file or the GRO file object + skip_bonds : bool, optional + If True, skip trying to assign bonds. This can save substantial time + when parsing large files with non-standard residue names. However, + no bonds are assigned. This is OK if, for instance, the GRO file is + being parsed simply for its coordinates. This will also reduce the + accuracy of assigned atomic numbers for typical ions. Default is + False. + + Returns + ------- + struct : :class:`Structure` + The Structure instance instantiated with *just* residues and atoms + populated (with coordinates) + """ + struct = Structure() + if isinstance(filename, str): + fileobj = genopen(filename, 'r') + own_handle = True + else: + fileobj = filename + own_handle = False + try: + # Ignore the title line + fileobj.readline() + try: + natom = int(fileobj.readline().strip()) + except ValueError: + raise GromacsError(f'Could not parse {filename} as GRO file') + line_parser = _AtomLineParser() + for i, line in enumerate(fileobj): + if i == natom: break + try: + atom, resname, resnum = line_parser.read(line) + except (ValueError, IndexError): + raise GromacsError(f'Could not parse the atom record of GRO file {filename}') + struct.add_atom(atom, resname, resnum) + else: + # If no box exists, the break did not hit, so line still + # contains the last atom (which cannot be interpreted as a box). + # This wipes out line (IFF fileobj reached the line) + line = fileobj.readline() + if i+1 != natom: + raise GromacsError(f'Truncated GRO file. Found {i+1} of {natom} atoms') + # Get the box from the last line if it's present + if line.strip(): + try: + box = [float(x) for x in line.split()] + except ValueError: + raise GromacsError(f'Could not understand box line of GRO file {filename}') + if len(box) == 3: + struct.box = [box[0]*10, box[1]*10, box[2]*10, 90.0, 90.0, 90.0] + elif len(box) == 9: + # Assume we have vectors + leng, ang = box_vectors_to_lengths_and_angles( + [box[0], box[3], box[4]] * u.nanometers, + [box[5], box[1], box[6]] * u.nanometers, + [box[7], box[8], box[2]] * u.nanometers, + ) + a, b, c = leng.value_in_unit(u.angstroms) + alpha, beta, gamma = ang.value_in_unit(u.degrees) + struct.box = [a, b, c, alpha, beta, gamma] + finally: + if own_handle: + fileobj.close() + + # Assign bonds (and improved element guesses) + if not skip_bonds: + struct.assign_bonds() + + return struct + + @staticmethod + def write(struct, dest, precision=3, nobox=False, combine=False): + """ Write a Gromacs Topology File from a Structure + + Parameters + ---------- + struct : :class:`Structure` + The structure to write to a Gromacs GRO file (must have coordinates) + dest : str or file-like + The name of a file or a file object to write the Gromacs topology to + precision : int, optional + The number of decimal places to print in the coordinates. Default 3 + nobox : bool, optional + If the system does not have a periodic box defined, and this option + is True, no box will be written. If False, the periodic box will be + defined to enclose the solute with 0.5 nm clearance on all sides. If + periodic box dimensions *are* defined, this variable has no effect. + combine : 'all', None, or list of iterables, optional + Equivalent to the combine argument of the GromacsTopologyFile.write + method. If None, system atom order may be changed to meet the need + for contiguously bonded groups of atoms to be part of a single + moleculetype. All other values leave the atom order unchanged. + Default is None. + """ + + def _write_atom_line(atom, atid, resid, has_vels, dest, precision): + varwidth = 5 + precision + crdfmt = f'{{:{varwidth}.{precision}f}}' + velfmt = f'{{:{varwidth}.{precision + 1}f}}' + dest.write(f'{resid:5d}{atom.residue.name[:5]:<5s}{atom.name[:5]:>5s}{atid:5d}') + dest.write(crdfmt.format(atom.xx / 10)[:varwidth]) + dest.write(crdfmt.format(atom.xy / 10)[:varwidth]) + dest.write(crdfmt.format(atom.xz / 10)[:varwidth]) + if has_vels: + dest.write(velfmt.format(atom.vx / 10)[:varwidth]) + dest.write(velfmt.format(atom.vy / 10)[:varwidth]) + dest.write(velfmt.format(atom.vz / 10)[:varwidth]) + dest.write('\n') + + own_handle = False + if isinstance(dest, str): + dest = genopen(dest, 'w') + own_handle = True + elif not hasattr(dest, 'write'): + raise TypeError('dest must be a file name or file-like object') + + dest.write('GROningen MAchine for Chemical Simulation\n') + dest.write(f'{len(struct.atoms):5d}\n') + has_vels = all(hasattr(a, 'vx') for a in struct.atoms) + if combine != 'all': + resid, atid = 0, 0 + # use struct.split to get residue order as per topology file + split_struct = struct.split() + n_mols = sum(len(mol[1]) for mol in split_struct) + unused_atoms = list(struct.atoms) + for molid in range(n_mols): + # loop through molids so we can get the correct molecule + # according to the order they appear + molecule = [mol[0] for mol in split_struct if molid in mol[1]][0] + new_molecule = set() # track atoms added + last_found_atom = None # track when gro and top diverge + + for residue in molecule.residues: + resid += 1 + for atom in residue.atoms: + # for each atom in split topology get the first + # matching occurrence in the original structure + for original_atom in unused_atoms: + if atom.type == original_atom.type and \ + atom.name == original_atom.name and \ + atom.residue.name == original_atom.residue.name: + + if last_found_atom is not None and \ + original_atom.idx != last_found_atom.idx + 1: + # a rearrangement has occurred! Need to do + # extra check that we've found the correct + # original_atom + if len(new_molecule.intersection(original_atom.bond_partners)) == 0: + # original_atom must be bonded to at + # least one atom in the molecule we + # are currently writing otherwise find + # next candidate + continue + + atid += 1 + _write_atom_line( + original_atom, atid % 100000, + resid % 100000, has_vels, dest, precision + ) + new_molecule.add(original_atom) + last_found_atom = original_atom + unused_atoms.remove(original_atom) + break + else: + raise RuntimeError(f"Could not find {atom}") + else: + for atom in struct.atoms: + resid = (atom.residue.idx + 1) % 100000 + atid = (atom.idx + 1) % 100000 + _write_atom_line(atom, atid, resid, has_vels, dest, precision) + + # Box, in the weird format... + if struct.box is not None: + a, b, c = reduce_box_vectors(*box_lengths_and_angles_to_vectors(*struct.box)) + if all([abs(x-90) < TINY for x in struct.box[3:]]): + dest.write(f'{a[0] / 10:10.5f}{b[1] / 10:10.5f}{c[2] / 10:10.5f}\n') + else: + dest.write( + f'{a[0] / 10:10.5f}{b[1] / 10:10.5f}{c[2] / 10:10.5f}{a[1] / 10:10.5f}{a[2] / 10:10.5f}' + f'{b[0] / 10:10.5f}{b[2] / 10:10.5f}{c[0] / 10:10.5f}{c[1] / 10:10.5f}\n' + ) + elif not nobox and struct.atoms: + # Find the extent of the molecule in all dimensions, and buffer it by 5 A + crds = struct.coordinates + diff = (crds.max(axis=0) - crds.min(axis=0)) / 10 + 0.5 + dest.write(f'{diff[0]:10.5f}{diff[1]:10.5f}{diff[2]:10.5f}\n') + if own_handle: + dest.close() diff --git a/pmx/workflow/parmed/gromacs/gromacstop.py b/pmx/workflow/parmed/gromacs/gromacstop.py new file mode 100644 index 00000000..3e47f74c --- /dev/null +++ b/pmx/workflow/parmed/gromacs/gromacstop.py @@ -0,0 +1,2100 @@ +""" +This module contains functionality relevant to loading a GROMACS topology file +and building a Structure from it +""" +from collections import OrderedDict, defaultdict +from contextlib import closing +import copy +from datetime import datetime +import math +import os +import re +from string import ascii_letters +import sys +import warnings + +from ..constants import TINY, DEG_TO_RAD +from ..exceptions import GromacsError, GromacsWarning, ParameterError +from ..formats.registry import FileFormatType +from ..parameters import ParameterSet, _find_ureybrad_key +from ._gromacsfile import GromacsFile +from ..structure import Structure +from ..topologyobjects import (Atom, Bond, Angle, Dihedral, Improper, + NonbondedException, ExtraPoint, BondType, Cmap, NoUreyBradley, + AngleType, DihedralType, DihedralTypeList, ImproperType, CmapType, + RBTorsionType, ThreeParticleExtraPointFrame, AtomType, UreyBradley, + TwoParticleExtraPointFrame, OutOfPlaneExtraPointFrame, + NonbondedExceptionType, UnassignedAtomType) +from ..periodic_table import element_by_mass, AtomicNum +from .. import unit as u +from ..utils.io import genopen + +try: + import pwd + try: + _username = pwd.getpwuid(os.getuid())[0] + except KeyError: + _username = 'username' + _userid = os.getuid() + _uname = os.uname()[1] +except ImportError: + import getpass + _username = getpass.getuser() # pragma: no cover + _userid = 0 # pragma: no cover + import platform # pragma: no cover + _uname = platform.node() # pragma: no cover + + + +# Gromacs uses "funct" flags in its parameter files to indicate what kind of +# functional form is used for each of its different parameter types. This is +# taken from the topdirs.c source code file along with a table in the Gromacs +# user manual. The table below summarizes my findings, for reference: + +# Bonds +# ----- +# 1 - F_BONDS : simple harmonic potential +# 2 - F_G96BONDS : fourth-power potential +# 3 - F_MORSE : morse potential +# 4 - F_CUBICBONDS : cubic potential +# 5 - F_CONNBONDS : not even implemented in GROMACS +# 6 - F_HARMONIC : seems to be the same as (1) ?? +# 7 - F_FENEBONDS : finietely-extensible-nonlinear-elastic (FENE) potential +# 8 - F_TABBONDS : bond function from tabulated function +# 9 - F_TABBONDSNC : bond function from tabulated function (no exclusions) +# 10 - F_RESTRBONDS : restraint bonds + +# Angles +# ------ +# 1 - F_ANGLES : simple harmonic potential +# 2 - F_G96ANGLES : cosine-based angle potential +# 3 - F_CROSS_BOND_BONDS : bond-bond cross term potential +# 4 - F_CROSS_BOND_ANGLES : bond-angle cross term potential +# 5 - F_UREY_BRADLEY : Urey-Bradley angle-bond potential +# 6 - F_QUARTIC_ANGLES : 4th-order polynomial potential +# 7 - F_TABANGLES : angle function from tabulated function +# 8 - F_LINEAR_ANGLES : angle function from tabulated function +# 9 - F_RESTRANGLES : restricted bending potential + +# Dihedrals +# --------- +# 1 - F_PDIHS : periodic proper torsion potential [ k(1+cos(n*phi-phase)) ] +# 2 - F_IDIHS : harmonic improper torsion potential +# 3 - F_RBDIHS : Ryckaert-Bellemans torsion potential +# 4 - F_PIDIHS : periodic harmonic improper torsion potential (same as 1) +# 5 - F_FOURDIHS : Fourier dihedral torsion potential +# 8 - F_TABDIHS : dihedral potential from tabulated function +# 9 - F_PDIHS : Same as 1, but can be multi-term +# 10 - F_RESTRDIHS : Restricted torsion potential +# 11 - F_CBTDIHS : combined bending-torsion potential + +_sectionre = re.compile(r'\[ (\w+) \]\s*$') + +class _Defaults(object): + """ Global properties of force fields as implemented in GROMACS """ + def __init__(self, nbfunc=1, comb_rule=2, gen_pairs='no', fudgeLJ=1.0, fudgeQQ=1.0): + if int(nbfunc) not in (1, 2): + raise ValueError('nbfunc must be 1 (L-J) or 2 (Buckingham)') + if int(comb_rule) not in (1, 2, 3): + raise ValueError('comb_rule must be 1, 2, or 3') + if gen_pairs not in ('yes', 'no'): + raise ValueError("gen_pairs must be 'yes' or 'no'") + if float(fudgeLJ) < 0: + raise ValueError('fudgeLJ must be non-negative') + if float(fudgeQQ) < 0: + raise ValueError('fudgeQQ must be non-negative') + self.nbfunc = int(nbfunc) + self.comb_rule = int(comb_rule) + self.gen_pairs = gen_pairs + self.fudgeLJ = float(fudgeLJ) + self.fudgeQQ = float(fudgeQQ) + + def __repr__(self): + return ('<_Defaults: nbfunc=%d, comb-rule=%d, gen-pairs="%s", ' + 'fudgeLJ=%g, fudgeQQ=%g>' % (self.nbfunc, self.comb_rule, + self.gen_pairs, self.fudgeLJ, self.fudgeQQ)) + + def __getitem__(self, idx): + # Treat it like the array that it is in the topology file + if idx < 0: idx += 5 + if idx == 0: + return self.nbfunc + if idx == 1: + return self.comb_rule + if idx == 2: + return self.gen_pairs + if idx == 3: + return self.fudgeLJ + if idx == 4: + return self.fudgeQQ + raise IndexError(f'Index {idx} out of range') + + def __eq__(self, other): + return (self.nbfunc == other.nbfunc and + self.comb_rule == other.comb_rule and + self.gen_pairs == other.gen_pairs and + self.fudgeLJ == other.fudgeLJ and + self.fudgeQQ == other.fudgeQQ) + + def __setitem__(self, idx, value): + if idx < 0: idx += 5 + if idx == 0: + if int(value) not in (1, 2): + raise ValueError('nbfunc must be 1 or 2') + self.nbfunc = int(value) + elif idx == 1: + if int(value) not in (1, 2, 3): + raise ValueError('comb_rule must be 1, 2, or 3') + self.comb_rule = int(value) + elif idx == 2: + if value not in ('yes', 'no'): + raise ValueError('gen_pairs must be "yes" or "no"') + self.gen_pairs = value + elif idx == 3: + if float(value) < 0: + raise ValueError('fudgeLJ must be non-negative') + self.fudgeLJ = float(value) + elif idx == 4: + if float(value) < 0: + raise ValueError('fudgeQQ must be non-negative') + self.fudgeQQ = value + else: + raise IndexError(f'Index {idx} out of range') + +class TopFromStructureMixin: + + @classmethod + def from_structure(cls, struct, copy=False): + """ Instantiates a GromacsTopologyFile instance from a Structure + + Parameters + ---------- + struct : :class:`parmed.Structure` + The input structure to generate from + copy : bool, optional + If True, assign from a *copy* of ``struct`` (this is a lot slower). + Default is False + + Returns + ------- + gmxtop : :class:`GromacsTopologyFile` + The topology file defined by the given struct + """ + from copy import copy as _copy + clstop = cls() + if copy: + struct = _copy(struct) + struct.join_dihedrals() + clstop.atoms = struct.atoms + clstop.residues = struct.residues + clstop.bonds = struct.bonds + clstop.angles = struct.angles + clstop.dihedrals = struct.dihedrals + clstop.impropers = struct.impropers + clstop.cmaps = struct.cmaps + clstop.rb_torsions = struct.rb_torsions + clstop.urey_bradleys = struct.urey_bradleys + clstop.adjusts = struct.adjusts + clstop.bond_types = struct.bond_types + clstop.angle_types = struct.angle_types + clstop.dihedral_types = struct.dihedral_types + clstop.improper_types = struct.improper_types + clstop.cmap_types = struct.cmap_types + clstop.rb_torsion_types = struct.rb_torsion_types + clstop.urey_bradley_types = struct.urey_bradley_types + clstop.adjust_types = struct.adjust_types + clstop.combining_rule = struct.combining_rule + clstop.box = struct.box + clstop.nrexcl = struct.nrexcl + if (struct.trigonal_angles or + struct.out_of_plane_bends or + struct.pi_torsions or + struct.stretch_bends or + struct.torsion_torsions or + struct.chiral_frames or + struct.multipole_frames): + raise TypeError('GromacsTopologyFile does not support Amoeba FF') + # Now check what the 1-4 scaling factors should be + if hasattr(struct, 'defaults') and isinstance(struct.defaults, _Defaults): + clstop.defaults = struct.defaults + else: + scee_values = set() + scnb_values = set() + if struct.adjusts: + for adjust in struct.adjusts: + if adjust.type is None: continue + scee_values.add(1/adjust.type.chgscale) + # Do not add scnb_values, since we can just set explicit + # exception pair parameters in GROMACS (which this structure + # already has) + # In order to specify specific pair parameters, we need to set + # gen_pairs to 'no' so that the pair-specific L-J parameters are + # printed to the topology file (rather than being auto-created) + clstop.defaults.gen_pairs = 'no' + else: + for dihedral in struct.dihedrals: + if dihedral.type is None or dihedral.ignore_end: continue + if isinstance(dihedral.type, DihedralTypeList): + for dt in dihedral.type: + if dt.scee: + scee_values.add(dt.scee) + if dt.scnb: + scnb_values.add(dt.scnb) + else: + if dihedral.type.scee: + scee_values.add(dihedral.type.scee) + if dihedral.type.scnb: + scnb_values.add(dihedral.type.scnb) + if len(set('%.5f' % x for x in scee_values)) > 1: + raise GromacsError('Structure has mixed 1-4 scaling which is not supported by Gromacs') + scee_values = list(scee_values) + scnb_values = list(scnb_values) + if len(set('%.5f' % x for x in scee_values)) == 1: + clstop.defaults.fudgeQQ = 1/scee_values[0] + else: + clstop.defaults.fudgeQQ = 1.0 + if len(set('%.5f' % x for x in scnb_values)) == 1: + clstop.defaults.fudgeLJ = 1/scnb_values[0] + else: + clstop.defaults.fudgeLJ = 1.0 + if clstop.combining_rule == 'geometric': + clstop.defaults.comb_rule = 3 + + clstop.parameterset = ParameterSet.from_structure(struct, allow_unequal_duplicates=True) + return clstop + +class GromacsTopologyFile(Structure, TopFromStructureMixin, metaclass=FileFormatType): + """ Class providing a parser and writer for a GROMACS topology file + + Parameters + ---------- + fname : str + The name of the file to read + defines : list of str=None + If specified, this is the set of defines to use when parsing the + topology file + parametrized : bool, optional + If True, parameters are assigned after parsing is done from the + parametertypes sections. If False, only parameter types defined in the + parameter sections themselves are loaded (i.e., on the same line as the + parameter was defined). Default is True + xyz : str or array, optional + The source of atomic coordinates. It can be a string containing the name + of a coordinate file from which to fill the coordinates (and optionally + the unit cell information), or it can be an array with the coordinates. + Default is None + box : array, optional + If provided, the unit cell information will be set from this variable. + If provided, it must be a collection of 6 floats representing the unit + cell dimensions a, b, c, alpha, beta, and gamma, respectively. Default + is None. + + Notes + ----- + If the ``xyz`` argument is a file name that contains the unit cell + information, this unit cell information is set. However, the ``box`` + argument takes precedence and will override values given in the coordinate + file unless it has its default value of ``None``. + """ + + #=================================================== + + @staticmethod + def id_format(filename): + """ Identifies the file as a GROMACS topology file + + Parameters + ---------- + filename : str + Name of the file to check if it is a gromacs topology file + + Returns + ------- + is_fmt : bool + If it is identified as a gromacs topology, return True. False + otherwise + """ + with closing(genopen(filename)) as f: + for line in f: + if line.startswith(';'): + line = line[:line.index(';')] + if not line.strip(): continue + if line.startswith('#'): + if line.startswith('#if'): continue + if line.startswith('#define'): continue + if line.startswith('#include'): continue + if line.startswith('#undef'): continue + if line.startswith('#endif'): continue + return False + rematch = _sectionre.match(line) + if not rematch: + return False + sec, = rematch.groups() + return sec in {'atoms', 'atomtypes', 'defaults', 'moleculetype', 'system', 'bondtypes', 'angletypes', + 'cmaptypes', 'dihedraltypes', 'bonds', 'angles', 'dihedrals', 'cmaps', 'molecules', + 'exclusions', 'nonbond_params', 'position_restraints'} + return False + + #=================================================== + + def __init__(self, fname=None, defines=None, parametrize=True, xyz=None, box=None): + from .. import load_file + super(GromacsTopologyFile, self).__init__() + self.parameterset = None + self.defaults = _Defaults(gen_pairs='yes') # make ParmEd's default yes + if fname is not None: + self.read(fname, defines, parametrize) + # Fill in coordinates and unit cell information if appropriate + if xyz is not None: + if isinstance(xyz, str): + f = load_file(xyz, skip_bonds=True) + if not hasattr(f, 'coordinates') or f.coordinates is None: + raise TypeError(f'File {xyz} does not have coordinates') + self.coordinates = f.coordinates + if box is None and hasattr(f, 'box'): + self.box = f.box + else: + self.coordinates = xyz + if box is not None: + self.box = box + self.unchange() + elif xyz is not None or box is not None: + raise ValueError('Cannot provide coordinates/box and NOT a top') + + #=================================================== + + def read(self, fname, defines=None, parametrize=True): + """ Reads the topology file into the current instance """ + from .. import gromacs as gmx + params = self.parameterset = ParameterSet() + molecules = self.molecules = dict() + bond_types = dict() + angle_types = dict() + ub_types = dict() + dihedral_types = dict() + exc_types = dict() + structure_contents = [] + molnames = [] + if defines is None: + defines = OrderedDict(FLEXIBLE=1) + proper_multiterm_dihedrals = dict() + with closing(GromacsFile(fname, includes=[gmx.GROMACS_TOPDIR], defines=defines)) as f: + current_section = None + for line in f: + line = line.strip() + if not line: continue + + if line[0] == '[': + current_section = line[1:-1].strip() + elif current_section == 'moleculetype': + molname, nrexcl = line.split() + nrexcl = int(nrexcl) + if molname in molecules: + raise GromacsError(f'Duplicate definition of molecule {molname}') + molecule = Structure() + molecules[molname] = (molecule, nrexcl) + molnames.append(molname) + molecule.nrexcl = nrexcl + bond_types = dict() + angle_types = dict() + ub_types = dict() + dihedral_types = dict() + exc_types = dict() + elif current_section == 'atoms': + atom, resname, resnum, icode = self._parse_atoms(line, params) + molecule.add_atom(atom, resname, resnum, inscode=icode) + elif current_section == 'bonds': + bond, bond_type = self._parse_bonds(line, bond_types, molecule.atoms) + molecule.bonds.append(bond) + if bond_type is not None: + molecule.bond_types.append(bond_type) + bond_type.list = molecule.bond_types + elif current_section == 'pairs': + nbe, nbet = self._parse_pairs(line, exc_types, molecule.atoms) + molecule.adjusts.append(nbe) + if nbet is not None: + molecule.adjust_types.append(nbet) + nbet.list = molecule.adjust_types + elif current_section == 'angles': + ang, ub, angt, ubt = self._parse_angles(line, angle_types, ub_types, + molecule.atoms) + molecule.angles.append(ang) + if ub is not None: + molecule.urey_bradleys.append(ub) + if angt is not None: + molecule.angle_types.append(angt) + angt.list = molecule.angle_types + if ubt is not None and ubt is not NoUreyBradley: + molecule.urey_bradley_types.append(ubt) + ubt.list = molecule.urey_bradley_types + elif current_section == 'dihedrals': + self._parse_dihedrals(line, dihedral_types, proper_multiterm_dihedrals, + molecule) + elif current_section == 'cmap': + cmap = self._parse_cmaps(line, molecule.atoms) + molecule.cmaps.append(cmap) + elif current_section == 'system': + self.title = line + elif current_section == 'defaults': + words = line.split() + if len(words) < 2: # 3, 4, and 5 fields are optional + raise GromacsError('Too few fields in [ defaults ]') + if words[0] != '1': + warnings.warn('Unsupported nonbonded type; unknown functional', + GromacsWarning) + self.unknown_functional = True + if words[1] in ('1', '3'): + self.combining_rule = 'geometric' + self.defaults = _Defaults(*words) + elif current_section == 'molecules': + name, num = line.split() + num = int(num) + structure_contents.append((name, num)) + elif current_section == 'settles': + bnds, bndts = self._parse_settles(line, molecule.atoms) + molecule.bonds.extend(bnds) + molecule.bond_types.extend(bndts) + molecule.bond_types.claim() + elif current_section in ('virtual_sites3', 'dummies3'): + try: + b, bt = self._parse_vsites3(line, molecule.atoms, params) + except KeyError: + raise GromacsError('Cannot determine vsite geometry ' + 'without parameter types') + molecule.bonds.append(b) + molecule.bond_types.append(bt) + bt.list = molecule.bond_types + elif current_section == 'exclusions': + atoms = [molecule.atoms[int(w)-1] for w in line.split()] + for a in atoms[1:]: + atoms[0].exclude(a) + elif current_section == 'atomtypes': + attype, typ = self._parse_atomtypes(line) + params.atom_types[attype] = typ + elif current_section == 'nonbond_params': + words = line.split() + a1, a2 = words[:2] +# func = int(words[2]) #... unused + sig, eps = (float(x) for x in words[3:5]) + sig *= 10 # Convert to Angstroms + eps *= u.kilojoule.conversion_factor_to(u.kilocalorie) + params.nbfix_types[(a1, a2)] = (eps, sig*2**(1/6)) + params.nbfix_types[(a2, a1)] = (eps, sig*2**(1/6)) + params.atom_types[a1].add_nbfix(a2, sig*2**(1/6), eps) + params.atom_types[a2].add_nbfix(a1, sig*2**(1/6), eps) + elif current_section == 'bondtypes': + a, b, t = self._parse_bondtypes(line) + params.bond_types[(a, b)] = t + params.bond_types[(b, a)] = t + elif current_section == 'angletypes': + a, b, c, t, ut = self._parse_angletypes(line) + params.angle_types[(a, b, c)] = t + params.angle_types[(c, b, a)] = t + if ut is not None: + params.urey_bradley_types[(a, b, c)] = ut + params.urey_bradley_types[(c, b, a)] = ut + elif current_section == 'dihedraltypes': + key, knd, t, replace = self._parse_dihedraltypes(line) + rkey = tuple(reversed(key)) + if knd == 'normal': + if replace or key not in params.dihedral_types: + t = DihedralTypeList([t]) + params.dihedral_types[key] = t + params.dihedral_types[rkey] = t + elif key in params.dihedral_types: + params.dihedral_types[key].append(t, override=True) + elif knd == 'improper': + params.improper_types[key] = t + elif knd == 'improper_periodic': + params.improper_periodic_types[key] = t + params.improper_periodic_types[rkey] = t + elif knd == 'rbtorsion': + params.rb_torsion_types[key] = t + params.rb_torsion_types[rkey] = t + elif current_section == 'cmaptypes': + a1, a2, a3, a4, a5, t = self._parse_cmaptypes(line) + params.cmap_types[(a1, a2, a3, a4, a2, a3, a4, a5)] = t + params.cmap_types[(a5, a4, a3, a2, a4, a3, a2, a1)] = t + elif current_section == 'pairtypes': + a, b, t = self._parse_pairtypes(line) + params.pair_types[(a, b)] = params.pair_types[(b, a)] = t + itplist = f.included_files + + # If the file did not contain the molecules section, perhaps + # because it was an itp-file. We assume that each molecule loaded + # should be contained once in this structure + if not structure_contents : + for name in molnames : + structure_contents.append((name, 1)) + + # Combine first, then parametrize. That way, we don't have to create + # copies of the ParameterType instances in self.parameterset + for molname, num in structure_contents: + if molname not in molecules: + raise GromacsError('Structure contains %s molecules, but no ' + 'template defined' % molname) + molecule, nrexcl = molecules[molname] + if nrexcl < 3 and _any_atoms_farther_than(molecule, nrexcl): + warnings.warn('nrexcl %d not currently supported' % nrexcl, + GromacsWarning) + elif nrexcl > 3 and _any_atoms_farther_than(molecule, 3): + warnings.warn('nrexcl %d not currently supported' % nrexcl, + GromacsWarning) + if num == 0: + warnings.warn('Detected addition of 0 %s molecules in topology ' + 'file' % molname, GromacsWarning) + if num == 1: + self += molecules[molname][0] + elif num > 1: + self += molecules[molname][0] * num + else: + raise GromacsError("Can't add %d %s molecules" % (num, molname)) + self.itps = itplist + if parametrize: + self.parametrize() + + #=================================================== + + # Private parsing helper functions + + def _parse_atoms(self, line, params): + """ Parses an atom line. Returns an Atom, resname, resnum """ + words = line.split() + try: + attype = params.atom_types[words[1]] + except KeyError: + attype = None + if len(words) < 8: + if attype is not None: + mass = attype.mass + atomic_number = attype.atomic_number + else: + mass = -1 + atomic_number = -1 + else: + mass = float(words[7]) + if attype is not None and attype.atomic_number >= 0: + atomic_number = attype.atomic_number + else: + atomic_number = AtomicNum[element_by_mass(mass)] + charge = float(words[6]) if len(words) > 6 else None + if atomic_number == 0: + atom = ExtraPoint(name=words[4], type=words[1], charge=charge) + else: + atom = Atom(atomic_number=atomic_number, name=words[4], + type=words[1], charge=charge, mass=mass) + + # check for insertion code and negative res number + if words[2].isnumeric() or (words[2].startswith('-') and words[2][1:].isnumeric()): + icode = '' + resnum = int(words[2]) + else: + icode = words[2][-1] + resnum = int(words[2][:-1]) + return atom, words[3], resnum, icode + + def _parse_bonds(self, line, bond_types, atoms): + """ Parses a bond line. Returns a Bond, BondType/None """ + words = line.split() + i, j = int(words[0])-1, int(words[1])-1 + funct = int(words[2]) + if funct != 1: + warnings.warn('bond funct != 1; unknown functional', + GromacsWarning) + self.unknown_functional = True + bond = Bond(atoms[i], atoms[j]) + bond.funct = funct + bond_type = None + if len(words) >= 5 and funct == 1: + req, k = (float(x) for x in words[3:5]) + if (req, k) in bond_types: + bond.type = bond_types[(req, k)] + else: + bond_type = BondType( + k*u.kilojoule_per_mole/u.nanometer**2/2, + req*u.nanometer + ) + bond_types[(req, k)] = bond.type = bond_type + return bond, bond_type + + def _parse_pairs(self, line, exc_types, atoms): + """ Parses a pairs line. Returns NonbondedException, NEType/None """ + words = line.split() + i, j = int(words[0])-1, int(words[1])-1 + funct = int(words[2]) + if funct != 1: + # This is not even supported in Gromacs + warnings.warn('pairs funct != 1; unknown functional', + GromacsWarning) + self.unknown_functional = True + nbe = NonbondedException(atoms[i], atoms[j]) + nbe.funct = funct + nbet = None + if funct == 1 and len(words) >= 5: + sig = float(words[3]) * 2**(1/6) + eps = float(words[4]) + if (sig, eps) in exc_types: + nbe.type = exc_types[(sig, eps)] + else: + nbet = NonbondedExceptionType(sig*u.nanometers, eps*u.kilojoules_per_mole, + self.defaults.fudgeQQ) + exc_types[(sig, eps)] = nbe.type = nbet + return nbe, nbet + + def _parse_angles(self, line, angle_types, ub_types, atoms): + """ Parse an angles line, Returns Angle, UB/None, and types """ + words = line.split() + i, j, k = [int(w)-1 for w in words[:3]] + funct = int(words[3]) + if funct not in (1, 5): + warnings.warn('angles funct != 1 or 5; unknown ' + 'functional', GromacsWarning) + self.unknown_functional = True + angt = ub = ubt = None + ang = Angle(atoms[i], atoms[j], atoms[k]) + ang.funct = funct + if funct == 5: + ub = UreyBradley(atoms[i], atoms[k]) + if (funct == 1 and len(words) >= 6) or (funct == 5 and len(words) >= 8): + theteq, k = (float(x) for x in words[4:6]) + if (theteq, k) in angle_types: + ang.type = angle_types[(theteq, k)] + else: + angt = AngleType(k*u.kilojoule_per_mole/u.radian**2/2, + theteq*u.degree) + angle_types[(theteq, k)] = ang.type = angt + if funct == 5 and len(words) >= 8: + ubreq, ubk = (float(x) for x in words[6:8]) + if ubk > 0: + if (ubreq, ubk) in ub_types: + ub.type = ub_types[(ubreq, ubk)] + else: + ubt = BondType( + ubk*u.kilojoule_per_mole/u.nanometer**2/2, + ubreq*u.nanometer, + ) + ub_types[(ubreq, ubk)] = ub.type = ubt + else: + ub.type = NoUreyBradley + return ang, ub, angt, ubt + + def _parse_dihedrals(self, line, dihedral_types, PMD, molecule): + """ Processes a dihedrals line, returns None """ + words = line.split() + i, j, k, l = [int(x)-1 for x in words[:4]] + funct = int(words[4]) + if funct in (1, 4) or (funct == 9 and len(words) < 8): + dih, diht = self._process_normal_dihedral(words, molecule.atoms, i, + j, k, l, dihedral_types, + funct==4) + molecule.dihedrals.append(dih) + if diht is not None: + molecule.dihedral_types.append(diht) + diht.list = molecule.dihedral_types + elif funct == 2: + dih, impt = self._process_improper(words, i, j, k, l, + molecule.atoms, dihedral_types) + molecule.impropers.append(dih) + if impt is not None: + molecule.improper_types.append(impt) + impt.list = molecule.improper_types + elif funct == 3: + dih, rbt = self._process_rbtorsion(words, i, j, k, l, molecule.atoms, + dihedral_types) + molecule.rb_torsions.append(dih) + if rbt is not None: + molecule.rb_torsion_types.append(rbt) + rbt.list = molecule.rb_torsion_types + elif funct == 9: + # in-line parameters, since len(words) must be >= 8 + key = (molecule.atoms[i], molecule.atoms[j], + molecule.atoms[k], molecule.atoms[l]) + if key in PMD: + diht = PMD[key] + self._process_dihedral_series(words, diht) + dih = None + else: + dih = Dihedral(*key) + diht = self._process_dihedral_series(words) + dih.type = PMD[key] = PMD[tuple(reversed(key))] = diht + molecule.dihedrals.append(dih) + molecule.dihedral_types.append(diht) + diht.list = molecule.dihedral_types + else: + # ??? unknown funct + warnings.warn('torsions funct != 1, 2, 3, 4, 9; unknown' + ' functional', GromacsWarning) + dih = Dihedral(molecule.atoms[i], molecule.atoms[j], + molecule.atoms[k], molecule.atoms[l]) + molecule.dihedrals.append(dih) + self.unknown_functional = True + + if dih is not None: + dih.funct = funct + + def _parse_cmaps(self, line, atoms): + """ Parses cmap terms, returns cmap """ + words = line.split() + i, j, k, l, m = (int(w)-1 for w in words[:5]) + funct = int(words[5]) + if funct != 1: + warnings.warn('cmap funct != 1; unknown functional', + GromacsWarning) + self.unknown_functional = True + cmap = Cmap(atoms[i], atoms[j], atoms[k], atoms[l], atoms[m]) + cmap.funct = funct + return cmap + + def _parse_settles(self, line, atoms): + """ Parses settles line; returns list of Bonds, list of BondTypes """ + # Instead of adding bonds that get constrained for waters (or other + # 3-atom molecules), GROMACS uses a "settles" section to specify the + # constraint geometry. We have to translate that into bonds. + natoms = len([a for a in atoms if not isinstance(a, ExtraPoint)]) + if natoms != 3: + raise GromacsError("Cannot SETTLE a %d-atom molecule" % natoms) + try: + oxy, = [atom for atom in atoms if atom.atomic_number == 8] + hyd1, hyd2 = [atom for atom in atoms if atom.atomic_number == 1] + except ValueError: + raise GromacsError('Can only SETTLE water; wrong atoms') + #TODO see if there's a bond_type entry in the parameter set + # that we can fill in? Wait until this is needed... + try: + i, funct, doh, dhh = line.split() + doh, dhh = float(doh), float(dhh) + except ValueError: + raise GromacsError('Bad [ settles ] line') + nm = u.nanometers + bt_oh = BondType(5e5*u.kilojoules_per_mole/nm**2, doh*nm) + bt_hh = BondType(5e5*u.kilojoules_per_mole/nm**2, dhh*nm) + return [Bond(oxy, hyd1, bt_oh), Bond(oxy, hyd2, bt_oh), + Bond(hyd1, hyd2, bt_hh)], [bt_oh, bt_hh] + + def _parse_vsites3(self, line, all_atoms, params): + """ Parse vsites3/dummy3 line; returns Bond, BondType """ + words = line.split() + vsite = all_atoms[int(words[0])-1] + atoms = [all_atoms[int(i)-1] for i in words[1:4]] + funct = int(words[4]) + if funct == 1: + a, b = float(words[5]), float(words[6]) + if abs(a - b) > TINY: + raise GromacsError("No vsite frames with different weights") + else: + raise GromacsError('Only 3-point vsite type 1 is supported') + # We need to know the geometry of the frame in order to + # determine the bond length between the virtual site and its + # parent atom + parent = atoms[0] + if vsite in parent.bond_partners: + raise GromacsError('Unexpected bond b/w vsite and its parent') + kws = dict() + for bond in parent.bonds: + if atoms[1] in bond: + key = (_gettype(parent), _gettype(atoms[1])) + kws['dp1'] = (bond.type or params.bond_types[key]).req + if atoms[2] in bond: + key = (_gettype(bond.atom1), _gettype(bond.atom2)) + kws['dp2'] = (bond.type or params.bond_types[key]).req + for angle in parent.angles: + if parent is not angle.atom2: continue + if atoms[0] not in angle or atoms[1] not in angle: continue + key = (_gettype(angle.atom1), _gettype(angle.atom2), + _gettype(angle.atom3)) + kws['theteq'] = (angle.type or params.angle_types[key]).theteq + break + else: # Did not break, no theta found + for bond in atoms[1].bonds: + if atoms[2] in bond: + key = (_gettype(bond.atom1), _gettype(bond.atom2)) + kws['d12'] = (bond.type or params.bond_types[key]).req + bondlen = ThreeParticleExtraPointFrame.from_weights(parent, atoms[1], + atoms[2], a, b, **kws) + bt_vs = BondType(0, bondlen*u.angstroms) + return Bond(vsite, parent, bt_vs), bt_vs + + def _parse_atomtypes(self, line): + """ Parses line from atomtypes section, returns str, AtomType """ + words = line.split() + # Support the following spec, found in the Gromacs source code: + # Field 0 (mandatory) : nonbonded type name (string) + # Field 1 (optional) : bonded type (string) + # Field 2 (optional) : atomic number (int) + # Field 3 (mandatory) : mass (float) + # Field 4 (mandatory) : charge (float) + # Field 5 (mandatory) : particle type (single character) + attype = words[0] + if len(words[3]) == 1 and words[3] in ascii_letters: + # Field 1 and Field 2 are both missing + atnum = -1 + sigidx = 4 +# ptypeidx = 3 # ... unused + massidx = 1 + bond_type = None + elif len(words[5]) == 1 and words[5] in ascii_letters: + # Both Field 1 and Field 2 are present + sigidx = 6 +# ptypeidx = 5 # ... unused + massidx = 3 + atnum = int(words[2]) + bond_type = words[1] + else: + # One of Field 1 or 2 are missing +# ptypeidx = 4 # ... unused + massidx = 2 + sigidx = 5 + try: + atnum = int(words[1]) + bond_type = None + except ValueError: + # This must be a bonded type string + bond_type = words[1] + atnum = -1 + mass = float(words[massidx]) + if mass > 0 and atnum == -1: + atnum = AtomicNum[element_by_mass(mass)] + chg = float(words[massidx+1]) +# ptype = words[ptypeidx] # ... unused + sig = float(words[sigidx]) * u.nanometers + eps = float(words[sigidx+1]) * u.kilojoules_per_mole + typ = AtomType(attype, None, mass, atnum, bond_type=bond_type, charge=chg) + typ.set_lj_params(eps, sig*2**(1/6)/2) + return attype, typ + + def _parse_bondtypes(self, line): + """ Parse bondtypes line. Returns str, str, BondType """ + words = line.split() + r = float(words[3]) * u.nanometers + k = (float(words[4]) / 2) * (u.kilojoules_per_mole / u.nanometers**2) + if words[2] != '1': + warnings.warn('bondtypes funct != 1; unknown functional', + GromacsWarning) + self.unknown_functional = True + return words[0], words[1], BondType(k, r) + + def _parse_angletypes(self, line): + """ + Parses angletypes line. Returns str, str, str, AngleType, BondType/None + """ + words = line.split() + theta = float(words[4]) * u.degrees + k = (float(words[5]) / 2) * (u.kilojoules_per_mole / u.radians**2) + if words[3] != '1' and words[3] != '5': + warnings.warn('angletypes funct != 1 or 5; unknown functional', + GromacsWarning) + self.unknown_functional = True + ub = None + if words[3] == '5': + # Contains the angle with urey-bradley + ub0 = float(words[6]) + cub = float(words[7]) / 2 + if cub == 0: + ub = NoUreyBradley + else: + ub0 *= u.nanometers + cub *= u.kilojoules_per_mole / u.nanometers**2 + ub = BondType(cub, ub0) + return words[0], words[1], words[2], AngleType(k, theta), ub + + def _parse_dihedraltypes(self, line): + """ Parse dihedraltypes, returns (str,str,str,str), str, Type, bool """ + words = line.split() + replace = False + dtype = 'normal' + # Ugh. Gromacs allows only two atom types (the middle atom types) to be + # specified. This signifies wild-cards + if words[2] in ('1', '2', '3', '4', '5', '8', '9', '10', '11'): + a1 = a4 = 'X' + a2, a3 = words[:2] + si = 2 + else: + a1, a2, a3, a4 = words[:4] + si = 4 + improper_periodic = False + replace = words[si] in ('1', '2', '3', '4') + improper_periodic = words[si] == '4' + if words[si] == '2': + dtype = 'improper' + elif words[si] == '3': + dtype = 'rbtorsion' + elif words[si] not in ('1', '4', '9'): + warnings.warn('dihedraltypes funct not supported', GromacsWarning) + self.unknown_functional = True + # Do the proper types + if dtype == 'normal': + phase = float(words[si+1]) * u.degrees + phi_k = float(words[si+2]) * u.kilojoules_per_mole + per = int(words[si+3]) + ptype = DihedralType(phi_k, per, phase, + scee=1/self.defaults.fudgeQQ, + scnb=1/self.defaults.fudgeLJ) + if improper_periodic: + # must do this here, since dtype has to be 'normal' above + dtype = 'improper_periodic' + elif dtype == 'improper': + theta = float(words[si+1])*u.degrees + k = float(words[si+2])*u.kilojoules_per_mole/u.radians**2/2 + a1, a2, a3, a4 = sorted([a1, a2, a3, a4]) + ptype = ImproperType(k, theta) + elif dtype == 'rbtorsion': + a1, a2, a3, a4 = words[:4] + c0, c1, c2, c3, c4, c5 = (float(x)*u.kilojoules_per_mole + for x in words[si+1:si+7]) + ptype = RBTorsionType(c0, c1, c2, c3, c4, c5, + scee=1/self.defaults.fudgeQQ, + scnb=1/self.defaults.fudgeLJ) + return (a1, a2, a3, a4), dtype, ptype, replace + + def _parse_cmaptypes(self, line): + words = line.split() + a1, a2, a3, a4, a5 = words[:5] +# funct = int(words[5]) # ... unused + res1, res2 = int(words[6]), int(words[7]) + grid = [float(w) for w in words[8:]] * u.kilojoules_per_mole + if len(grid) != res1 * res2: + raise GromacsError('CMAP grid dimensions do not match resolution') + if res1 != res2: + raise GromacsError('Only square CMAPs are supported') + return a1, a2, a3, a4, a5, CmapType(res1, grid) + + def _parse_pairtypes(self, line): + words = line.split() + a1, a2 = words[:2] +# funct = int(words[2]) # ... unused + cs6, cs12 = (float(x) for x in words[3:5]) + cs6 *= u.nanometers * 2**(1/6) + cs12 *= u.kilojoules_per_mole + return a1, a2, NonbondedExceptionType(cs6, cs12, self.defaults.fudgeQQ) + + #=================================================== + + # Internal Dihedral processing routines for different kinds of dihedrals + + def _process_normal_dihedral(self, words, atoms, i, j, k, l, + dihedral_types, imp): + dih = Dihedral(atoms[i], atoms[j], atoms[k], atoms[l], improper=imp) + diht = None + if len(words) >= 8: + phase, phi_k, per = (float(x) for x in words[5:8]) + if (phase, phi_k, per) in dihedral_types: + dih.type = dihedral_types[(phase, phi_k, per)] + else: + diht = DihedralType(phi_k*u.kilojoule_per_mole, + per, phase*u.degrees, + scee=1/self.defaults.fudgeQQ, + scnb=1/self.defaults.fudgeLJ) + dihedral_types[(phase, phi_k, per)] = dih.type = diht + return dih, diht + + def _process_dihedral_series(self, words, dihtype=None): + phase, phi_k, per = (float(x) for x in words[5:8]) + dt = DihedralType(phi_k*u.kilojoule_per_mole, + per, phase*u.degrees, + scee=1/self.defaults.fudgeQQ, + scnb=1/self.defaults.fudgeLJ) + if dihtype is not None: + dihtype.append(dt) + dtl = None + else: + dt = DihedralType(phi_k*u.kilojoule_per_mole, + per, phase*u.degrees, + scee=1/self.defaults.fudgeQQ, + scnb=1/self.defaults.fudgeLJ) + dtl = DihedralTypeList() + dtl.append(dt) + return dtl + + def _process_improper(self, words, i, j, k, l, atoms, dihedral_types): + """ Processes an improper, returns Improper, ImproperType """ + # Improper + imp = Improper(atoms[i], atoms[j], atoms[k], atoms[l]) + impt = None + if len(words) >= 7: + psieq, k = (float(x) for x in words[5:7]) + if (psieq, k) in dihedral_types: + imp.type = dihedral_types[(psieq, k)] + else: + impt = ImproperType(k*u.kilojoule_per_mole/u.radian**2/2, + psieq*u.degree) + imp.type = dihedral_types[(psieq, k)] = impt + return imp, impt + + def _process_rbtorsion(self, words, i, j, k, l, atoms, dihedral_types): + rb = Dihedral(atoms[i], atoms[j], atoms[k], atoms[l]) + rbt = None + if len(words) >= 11: + c0, c1, c2, c3, c4, c5 = (float(x) for x in words[5:11]) + if (c0, c1, c2, c3, c4, c5) in dihedral_types: + rb.type = dihedral_types[(c0, c1, c2, c3, c4, c5)] + else: + kjpm = u.kilojoules_per_mole + rbt = RBTorsionType(c0*kjpm, c1*kjpm, c2*kjpm, + c3*kjpm, c4*kjpm, c5*kjpm, + scee=1/self.defaults.fudgeQQ, + scnb=1/self.defaults.fudgeLJ) + dihedral_types[(c0, c1, c2, c3, c4, c5)] = rb.type = rbt + return rb, rbt + + #=================================================== + + def parametrize(self): + """ + Assign parameters to the current structure. This should be called + *after* `read` + """ + if self.parameterset is None: + raise RuntimeError('parametrize called before read') + params = copy.copy(self.parameterset) + def update_typelist_from(ptypes, types): + added_types = set(id(typ) for typ in types) + for k, typ in ptypes.items(): + if not typ.used: continue + if id(typ) in added_types: continue + added_types.add(id(typ)) + types.append(typ) + types.claim() + # Assign all of the parameters. If they've already been assigned (i.e., + # on the parameter line itself) keep the existing parameters + for atom in self.atoms: + atom.atom_type = params.atom_types[atom.type] + # The list of ordered 2-tuples of atoms explicitly specified in [ pairs ]. + # Under most circumstances, this is the list of 1-4 pairs. + gmx_pair = set() + for pair in self.adjusts: + if pair.atom1 > pair.atom2: + gmx_pair.add((pair.atom2, pair.atom1)) + else: + gmx_pair.add((pair.atom1, pair.atom2)) + if pair.type is not None: continue + key = (_gettype(pair.atom1), _gettype(pair.atom2)) + if key in params.pair_types: + pair.type = params.pair_types[key] + pair.type.used = True + elif self.defaults.gen_pairs == 'yes': + assert self.combining_rule in ('geometric', 'lorentz'), \ + 'Unrecognized combining rule' + if self.combining_rule == 'geometric': + eps = math.sqrt(pair.atom1.epsilon * pair.atom2.epsilon) + sig = math.sqrt(pair.atom1.sigma * pair.atom2.sigma) + elif self.combining_rule == 'lorentz': + eps = math.sqrt(pair.atom1.epsilon * pair.atom2.epsilon) + sig = 0.5 * (pair.atom1.sigma + pair.atom2.sigma) + eps *= self.defaults.fudgeLJ + pairtype = NonbondedExceptionType(sig*2**(1/6), eps, + self.defaults.fudgeQQ, list=self.adjust_types) + self.adjust_types.append(pairtype) + pair.type = pairtype + pair.type.used = True + else: + raise ParameterError('Not all pair parameters can be found') + update_typelist_from(params.pair_types, self.adjust_types) + # This is the list of 1-4 pairs determined from the bond graph. + # If this is different from what's in [ pairs ], we print a warning + # and make some adjustments (specifically, other programs assume + # the 1-4 list is complete, so we zero out the parameters for + # 1-4 pairs that aren't in [ pairs ]. + true_14 = set() + for bond in self.bonds: + for bpi in bond.atom1.bond_partners: + for bpj in bond.atom2.bond_partners: + if len(set([bpi, bond.atom1, bond.atom2, bpj])) < 4: + continue + if bpi in bpj.bond_partners or bpi in bpj.angle_partners: + continue + if bpi > bpj: + true_14.add((bpj, bpi)) + else: + true_14.add((bpi, bpj)) + if bond.type is not None: continue + key = (_gettype(bond.atom1), _gettype(bond.atom2)) + if key in params.bond_types: + bond.type = params.bond_types[key] + bond.type.used = True + else: + raise ParameterError('Not all bond parameters found') + if len(true_14 - gmx_pair) > 0: + zero_pairtype = NonbondedExceptionType(0.0, 0.0, 0.0, + list=self.adjust_types) + self.adjust_types.append(zero_pairtype) + num_zero_14 = 0 + for a1, a2 in (true_14 - gmx_pair): + self.adjusts.append(NonbondedException(a1, a2, zero_pairtype)) + num_zero_14 += 1 + warnings.warn('%i 1-4 pairs were missing from the [ pairs ] ' + 'section and were set to zero; make sure you ' + 'know what you\'re doing!' % num_zero_14, + GromacsWarning) + if len(gmx_pair - true_14) > 0: + warnings.warn('The [ pairs ] section contains %i exceptions that ' + 'aren\'t 1-4 pairs; make sure you know what ' + 'you\'re doing!' % (len(gmx_pair - true_14)), + GromacsWarning) + update_typelist_from(params.bond_types, self.bond_types) + for angle in self.angles: + if angle.type is not None: continue + key = (_gettype(angle.atom1), _gettype(angle.atom2), + _gettype(angle.atom3)) + if key in params.angle_types: + angle.type = params.angle_types[key] + angle.type.used = True + else: + raise ParameterError('Not all angle parameters found') + update_typelist_from(params.angle_types, self.angle_types) + for ub in self.urey_bradleys: + if ub.type is not None: continue + key = _find_ureybrad_key(ub) + if key in params.urey_bradley_types: + ub.type = params.urey_bradley_types[key] + if ub.type is not NoUreyBradley: + ub.type.used = True + else: + raise ParameterError('Not all urey-bradley parameters found') + # Now strip out all of the Urey-Bradley terms whose parameters are 0 + for i in reversed(range(len(self.urey_bradleys))): + if self.urey_bradleys[i].type is NoUreyBradley: + del self.urey_bradleys[i] + update_typelist_from(params.urey_bradley_types, self.urey_bradley_types) + for t in self.dihedrals: + if t.type is not None: continue + key = (_gettype(t.atom1), _gettype(t.atom2), _gettype(t.atom3), + _gettype(t.atom4)) + if not t.improper: + wckey = ('X', _gettype(t.atom2), _gettype(t.atom3), 'X') + wckey1 = (_gettype(t.atom1), _gettype(t.atom2), + _gettype(t.atom3), 'X') + wckey2 = ('X', _gettype(t.atom2), _gettype(t.atom3), + _gettype(t.atom4)) + if key in params.dihedral_types: + t.type = params.dihedral_types[key] + t.type.used = True + elif wckey1 in params.dihedral_types: + t.type = params.dihedral_types[wckey1] + t.type.used = True + elif wckey2 in params.dihedral_types: + t.type = params.dihedral_types[wckey2] + t.type.used = True + elif wckey in params.dihedral_types: + t.type = params.dihedral_types[wckey] + t.type.used = True + else: + raise ParameterError('Not all torsion parameters found') + else: + if key in params.improper_periodic_types: + t.type = params.improper_periodic_types[key] + t.type.used = True + else: + for wckey in [(key[0],key[1],key[2],'X'), + ('X',key[1],key[2],key[3]), + (key[0],key[1],'X','X'), + ('X','X',key[2],key[3])]: + if wckey in params.improper_periodic_types: + t.type = params.improper_periodic_types[wckey] + t.type.used = True + break + else: + raise ParameterError('Not all improper torsion ' + 'parameters found') + update_typelist_from(params.dihedral_types, self.dihedral_types) + update_typelist_from(params.improper_periodic_types, self.dihedral_types) + for t in self.rb_torsions: + if t.type is not None: continue + key = (_gettype(t.atom1), _gettype(t.atom2), _gettype(t.atom3), + _gettype(t.atom4)) + wckey = ('X', _gettype(t.atom2), _gettype(t.atom3), 'X') + wckey1 = (_gettype(t.atom1), _gettype(t.atom2), + _gettype(t.atom3), 'X') + wckey2 = ('X', _gettype(t.atom2), _gettype(t.atom3), + _gettype(t.atom4)) + if key in params.rb_torsion_types: + t.type = params.rb_torsion_types[key] + t.type.used = True + elif wckey1 in params.rb_torsion_types: + t.type = params.rb_torsion_types[wckey1] + t.type.used = True + elif wckey2 in params.rb_torsion_types: + t.type = params.rb_torsion_types[wckey2] + t.type.used = True + elif wckey in params.rb_torsion_types: + t.type = params.rb_torsion_types[wckey] + t.type.used = True + else: + raise ParameterError('Not all R-B torsion parameters found') + update_typelist_from(params.rb_torsion_types, self.rb_torsion_types) + self.update_dihedral_exclusions() + for t in self.impropers: + if t.type is not None: continue + key = tuple(sorted([_gettype(t.atom1), _gettype(t.atom2), + _gettype(t.atom3), _gettype(t.atom4)])) + if key in params.improper_types: + t.type = params.improper_types[key] + t.type.used = True + continue + # Now we will try to find a compatible wild-card... the first atom + # is the central atom. So take each of the other three and plug that + # one in + for anchor in (_gettype(t.atom2), _gettype(t.atom3), + _gettype(t.atom4)): + wckey = tuple(sorted([_gettype(t.atom1), anchor, 'X', 'X'])) + if wckey not in params.improper_types: continue + t.type = params.improper_types[wckey] + t.type.used = True + break + else: + raise ParameterError('Not all improper parameters found') + update_typelist_from(params.improper_types, self.improper_types) + for c in self.cmaps: + if c.type is not None: continue + key = (_gettype(c.atom1), _gettype(c.atom2), _gettype(c.atom3), + _gettype(c.atom4), _gettype(c.atom5)) + key = (key[0],key[1],key[2],key[3],key[1],key[2],key[3],key[4]) + if key in params.cmap_types: + c.type = params.cmap_types[key] + c.type.used = True + else: + raise ParameterError('Not all cmap parameters found') + update_typelist_from(params.cmap_types, self.cmap_types) + + #=================================================== + + def copy(self, cls, split_dihedrals=False): + """ + Makes a copy of the current structure as an instance of a specified + subclass + + Parameters + ---------- + cls : Structure subclass + The returned object is a copy of this structure as a `cls` instance + split_dihedrals : ``bool`` + If True, then the Dihedral entries will be split up so that each one + is paired with a single DihedralType (rather than a + DihedralTypeList) + + Returns + ------- + *cls* instance + The instance of the Structure subclass `cls` with a copy of the + current Structure's topology information + """ + c = super(GromacsTopologyFile, self).copy(cls, split_dihedrals) + c.defaults = copy.copy(self.defaults) + return c + + #=================================================== + + def __getitem__(self, selection): + """ See Structure.__getitem__ for documentation """ + # Make sure defaults is properly copied + struct = super(GromacsTopologyFile, self).__getitem__(selection) + if isinstance(struct, Atom): + return struct + struct.defaults = copy.copy(self.defaults) + return struct + + #=================================================== + + def write(self, dest, combine=None, parameters='inline', molfile=None, itp=False): + """ Write a Gromacs Topology File from a Structure + + Parameters + ---------- + dest : str or file-like + The name of a file or a file object to write the Gromacs topology to + combine : 'all', None, or list of iterables, optional + If None, no molecules are combined into a single moleculetype. If + 'all', all molecules are combined into a single moleculetype. + Otherwise, the list of molecule indices (start from 0) will control + which atoms are combined into single moleculetype's. Default is None + parameters : 'inline' or str or file-like object, optional + This specifies where parameters should be printed. If 'inline' + (default), the parameters are written on the same lines as the + valence terms are defined on. Any other string is interpreted as a + filename for an ITP that will be written to and then included at the + top of `dest`. If it is a file-like object, parameters will be + written there. If parameters is the same as ``dest``, then the + parameter types will be written to the same topologyfile. + molfile : None or str of file-like object, optional + If specified as other than None, the molecules will be written to a + separate file that is included in the main topology file. The + name of this file will be the provided srting. If None or + the same as the ``dest'', the molecules will be written into the + body of the topology file. If it is a file-like object, + the molecules will be written there. Using this option can make + it easier to combine multiple molecules into the same topology. + This will change where the following topology sections are + written: moleculetype, atoms, bonds, pairs, angles, dihedrals, + cmap, settles, virtual_sites2, virtual_sites3 and exclusions. + itp : bool, optional + If True the following topology sections are not written: + defaults, atomtypes, nonbond_params, bondtypes, pairtypes, + angletypes, dihedraltypes, cmaptypes, system and molecules + Thus only the individual molecules will be written in a stand-alone + fashion, i.e. an itp-file. + If True the molfile parameter will be set to None + + Raises + ------ + ValueError if the same molecule number appears in multiple combine lists + TypeError if the dest input cannot be parsed + ValueError if the combine, parameters, or molfile input cannot be parsed + """ + import parmed.gromacs as gmx + from .. import __version__ + own_handle = False + fname = '' + params = ParameterSet.from_structure(self, allow_unequal_duplicates=True) + if isinstance(dest, str): + fname = '%s ' % dest + dest = genopen(dest, 'w') + own_handle = True + elif not hasattr(dest, 'write'): + raise TypeError('dest must be a file name or file-like object') + + # Determine where to write the parameters + own_parfile_handle = False + include_parfile = None + if parameters == 'inline': + parfile = dest + elif isinstance(parameters, str): + if parameters == fname.strip(): + parfile = dest + else: + own_parfile_handle = True + parfile = genopen(parameters, 'w') + include_parfile = parameters + elif hasattr(parameters, 'write'): + parfile = parameters + else: + raise ValueError('parameters must be "inline", a file name, or a file-like object') + + # Determine where to write the molecules + if itp : + molfile = None + own_molfile_handle = False + include_molfile = None + if molfile is None: + _molfile = dest + elif isinstance(molfile, str): + if molfile == fname.strip(): + _molfile = dest + else: + own_molfile_handle = True + _molfile = genopen(molfile, 'w') + include_molfile = molfile + elif hasattr(molfile, 'write'): + _molfile = molfile + include_molfile = _molfile.name + # I assume the file should still be included even if it's not passed + # in as a file name. I'm not sure if all `write`-able objects have a + # `name` property, though. + else: + raise ValueError('molfile must be "top", a file name, or a file-like object') + + # Error-checking for combine + if combine is not None: + if isinstance(combine, str): + if combine.lower() != 'all': + raise ValueError('combine must be None, list of indices, or "all"') + else: + combine_lists = [] + for indices in combine: + indices = sorted(set(indices)) + if any((indices[i+1] - indices[i]) != 1 for i in range(len(indices)-1)): + raise ValueError('Can only combine adjacent molecules') + combine_lists.append(indices) + try: + # Write the header + now = datetime.now() + dest.write(f'''\ +; +; File {fname} was generated +; By user: {_username} ({_userid}) +; On host:{_uname} +; At date:{now.strftime('%a. %B %w %X %Y')} +; +; This is a standalone topology file +; +; Created by: +; ParmEd: {os.path.split(sys.argv[0])[1]}, VERSION{__version__} +; Executable: {os.path.split(sys.argv[0])[1]} +; Library dir: {gmx.GROMACS_TOPDIR} +; Command line: +; {(' '.join(sys.argv)).encode('unicode_escape').decode('utf-8')} +; +''') + if not itp : + dest.write('\n[ defaults ]\n') + dest.write('; nbfunc comb-rule gen-pairs fudgeLJ fudgeQQ\n') + dest.write( + f'{self.defaults.nbfunc:<15d} {self.defaults.comb_rule:<15d} {self.defaults.gen_pairs:<15s} ' + f'{self.defaults.fudgeLJ:<12.8g} {self.defaults.fudgeQQ:<12.8g}\n\n' + ) + if include_parfile is not None: + dest.write(f'#include "{include_parfile}"\n\n') + # Print all atom types + if not itp : + parfile.write('[ atomtypes ]\n') + if any(typ._bond_type is not None + for key, typ in params.atom_types.items()): + print_bond_types = True + else: + print_bond_types = False + if all(typ.atomic_number != -1 + for key, typ in params.atom_types.items()): + print_atnum = True + else: + print_atnum = False + parfile.write('; name ') + if print_bond_types: + parfile.write('bond_type ') + if print_atnum: + parfile.write('at.num ') + parfile.write('mass charge ptype sigma epsilon\n') + econv = u.kilocalories.conversion_factor_to(u.kilojoules) + for key, atom_type in params.atom_types.items(): + parfile.write(f'{str(atom_type):<7s} ') + if print_bond_types: + parfile.write(f'{str(atom_type.bond_type):<8s} ') + if print_atnum: + parfile.write('%8d ' % atom_type.atomic_number) + parfile.write('%10.6f %10.8f A %14.8g %14.8g\n' % ( + atom_type.mass, atom_type.charge, atom_type.sigma/10, + atom_type.epsilon*econv)) + parfile.write('\n') + # Nonbonded parameters + if not itp and self.has_NBFIX(): + typemap = dict(self.parameterset.nbfix_types) + types_in_system = self.parameterset.atom_types.keys() + dest.write('[ nonbond_params ]\n') + eps_conversion = u.kilocalorie.conversion_factor_to(u.kilojoule) + for key, val in typemap.items(): + if key[0] in types_in_system and key[1] in types_in_system: + eps = val[0] # kcal + sig = val[1] # Angstrom + eps *= eps_conversion + sig *= 0.1 + dest.write(f'{key[0]} {key[1]} 1 {sig / 2**(1/6)} {eps}\n') + # Print all parameter types unless we asked for inline + if not itp and parameters != 'inline': + if params.bond_types: + parfile.write('[ bondtypes ]\n') + parfile.write('; i j func b0 kb\n') + used_keys = set() + conv = (u.kilocalorie/u.angstrom**2).conversion_factor_to( + u.kilojoule/u.nanometer**2) * 2 + for key, param in params.bond_types.items(): + if key in used_keys: continue + used_keys.add(key) + used_keys.add(tuple(reversed(key))) + parfile.write(f'{key[0]:<5s} {key[1]:<5s} 1 {param.req / 10:.5f} {param.k * conv:f}\n') + parfile.write('\n') + if params.pair_types and self.defaults.gen_pairs == 'no': + parfile.write('[ pairtypes ]\n') + parfile.write('; i j func sigma1-4 epsilon1-4 ; THESE ARE 1-4 INTERACTIONS\n') + econv = u.kilocalorie.conversion_factor_to(u.kilojoule) + lconv = u.angstrom.conversion_factor_to(u.nanometer) + used_keys = set() + for key, param in params.pair_types.items(): + if key in used_keys: + continue + used_keys.add(key) + used_keys.add(tuple(reversed(key))) + parfile.write( + f'{key[0]:<5s} {key[1]:<5s} 1 {param.sigma * lconv:.9f} {param.epsilon * econv:.9f}\n' + ) + parfile.write('\n') + if params.angle_types: + parfile.write('[ angletypes ]\n') + parfile.write('; i j k func th0 cth rub kub\n') + used_keys = set() + conv = (u.kilocalorie/u.radian**2).conversion_factor_to( + u.kilojoule/u.radian**2) * 2 + bconv = (u.kilocalorie/u.angstrom**2).conversion_factor_to( + u.kilojoule/u.nanometer**2) * 2 + for key, param in params.angle_types.items(): + if key in used_keys: + continue + used_keys.add(key) + used_keys.add(tuple(reversed(key))) + part = '%-5s %-5s %-5s %%d %12.7f %12.7f' % ( + key[0], key[1], key[2], param.theteq, param.k*conv) + if key in params.urey_bradley_types: + ub = params.urey_bradley_types[key] + parfile.write(part % 5) + parfile.write(' %12.7f %12.7f\n' % (0.1*ub.req, ub.k*bconv)) + else: + parfile.write(part % 1) + parfile.write('\n') + parfile.write('\n') + if params.dihedral_types: + parfile.write('[ dihedraltypes ]\n') + parfile.write(';i j k l func phase kd pn\n') + used_keys = set() + conv = u.kilocalories.conversion_factor_to(u.kilojoules) + fmt = '%-6s %-6s %-6s %-6s %d %.2f %.6f %d\n' + for key, param in params.dihedral_types.items(): + if key in used_keys: continue + used_keys.add(key) + used_keys.add(tuple(reversed(key))) + for dt in param: + parfile.write(fmt % (key[0], key[1], key[2], key[3], 9, dt.phase, + dt.phi_k*conv, int(dt.per))) + parfile.write('\n') + if params.improper_periodic_types: + parfile.write('[ dihedraltypes ]\n') + parfile.write(';i j k l func phase kd pn\n') + used_keys = set() + conv = u.kilojoules.conversion_factor_to(u.kilocalories) + fmt = '%-6s %-6s %-6s %-6s %d %.2f %.6f %d\n' + for key, param in params.improper_periodic_types.items(): + if key in used_keys: continue + used_keys.add(key) + used_keys.add(tuple(reversed(key))) + parfile.write(fmt % (key[0], key[1], key[2], key[3], + 4, param.phase, param.phi_k*conv, int(param.per))) + parfile.write('\n') + if params.improper_types: + # BUGBUG -- The ordering is borked here because that made it + # simpler for me to work with back when I wrote the CHARMM + # parsers. This needs to be fixed now and handled correctly. + parfile.write('[ dihedraltypes ]\n') + parfile.write('; i j k l func q0 cq\n') + fmt = '%-6s %-6s %-6s %-6s %d %.6f %.6f\n' + conv = u.kilocalories.conversion_factor_to(u.kilojoules)*2 + for key, param in params.improper_types.items(): + parfile.write(fmt % (key[0], key[1], key[2], key[3], + 2, param.psi_eq, param.psi_k*conv)) + parfile.write('\n') + # CMAP grids are never printed inline, so if we have them, we need + # to write a dedicated section for them + if not itp and params.cmap_types: + parfile.write('[ cmaptypes ]\n\n') + used_keys = set() + conv = u.kilocalories.conversion_factor_to(u.kilojoules) + for key, param in params.cmap_types.items(): + if key in used_keys: continue + used_keys.add(key) + used_keys.add(tuple(reversed(key))) + parfile.write( + f'{key[0]:<6s} {key[1]:<6s} {key[2]:<6s} {key[3]:<6s} {key[7]:<6s} ' + f'1 {param.resolution:4d} {param.resolution:4d}' + ) + res2 = param.resolution * param.resolution + for i in range(0, res2, 10): + parfile.write('\\\n') + end = min(i+10, res2) + parfile.write(' '.join(str(param.grid[j]*conv) for j in range(i, end))) + parfile.write('\n\n') + if include_molfile is not None: + dest.write(f'#include "{include_molfile}"\n\n') + if combine is None: + molecules = self.split() + sysnum = 1 + names = [] + nameset = set() + for molecule, num in molecules: + if len(molecule.residues) == 1: + title = molecule.residues[0].name if molecule.residues[0].name.strip() else "UNK" + if title in nameset: + orig = title + sfx = 2 + while title in nameset: + title = f'{orig}{sfx}' + sfx += 1 + else: + title = f'system{sysnum}' + sysnum += 1 + names.append(title) + nameset.add(title) + GromacsTopologyFile._write_molecule(molecule, _molfile, title, params, parameters == 'inline') + if not itp : + # System + dest.write('[ system ]\n; Name\n') + if self.title: + dest.write(self.title) + else: + dest.write('Generic title') + dest.write('\n\n') + # Molecules + dest.write('[ molecules ]\n; Compound #mols\n') + total_mols = sum(len(m[1]) for m in molecules) + i = 0 + while i < total_mols: + for j, (molecule, lst) in enumerate(molecules): + if i in lst: + break + else: + raise AssertionError(f'Could not find molecule {i:d} in list') + ii = i + while ii < total_mols and ii in lst: + ii += 1 + dest.write(f'{names[j]:<15s} {ii - i:6d}\n') + i = ii + elif isinstance(combine, str) and combine.lower() == 'all': + GromacsTopologyFile._write_molecule(self, _molfile, 'system', params, parameters == 'inline') + if not itp : + dest.write('[ system ]\n; Name\n') + if self.title: + dest.write(self.title) + else: + dest.write('Generic title') # pragma: no cover + dest.write('\n\n') + # Molecules + dest.write('[ molecules ]\n; Compound #mols\n') + dest.write('%-15s %6d\n' % ('system', 1)) + else: + molecules = self.split() + nmols = sum(len(m[1]) for m in molecules) + moleculedict = dict() + # Hash our molecules by indices + for m, num in molecules: + for i in num: + moleculedict[i] = m + combined_molecules = [] + for cl in combine_lists: + counts = defaultdict(int) + mols_in_mol = [] + for molid in cl: + try: + mol = moleculedict[molid] + except KeyError: + raise IndexError('Molecule ID out of range') + counts[id(moleculedict[molid])] += 1 + if counts[id(moleculedict[molid])] == 1: + mols_in_mol.append(mol) + if counts[id(mols_in_mol[0])] > 1: + combmol = mols_in_mol[0] * counts[id(mols_in_mol[0])] + else: + combmol = copy.copy(mols_in_mol[0]) + for i, mol in enumerate(mols_in_mol): + if i == 0: continue + assert id(mol) in counts and counts[id(mol)] > 0 + if counts[id(mol)] > 1: + combmol += mol * counts[id(mol)] + else: + combmol += mol + combined_molecules.append((combmol, cl[0], len(cl))) + nmols -= (len(cl) - 1) + # combined_molecules now contains a list of tuples, and that + # tuple stores the combined molecule, first molecule index of + # the pre-combined molecule, and how many molecules were + # combined + + # Sort combined molecules by starting location + combined_molecules.sort(key=lambda x: x[1]) + new_molecules = [] + counts = defaultdict(set) + cmc = 0 # Combined Molecule Counter + add = 0 # How many molecules to "skip" due to combining + for i in range(nmols): + ii = i + add + if cmc < len(combined_molecules) and combined_molecules[cmc][1] == ii: + new_molecules.append([combined_molecules[cmc][0], set([i])]) + add += combined_molecules[cmc][2] - 1 + cmc += 1 + elif len(counts[id(moleculedict[ii])]) == 0: + counts[id(moleculedict[ii])].add(i) + new_molecules.append([moleculedict[ii], counts[id(moleculedict[ii])]]) + else: + counts[id(moleculedict[ii])].add(i) + sysnum = 1 + names = [] + nameset = set() + for molecule, num in new_molecules: + if len(molecule.residues) == 1: + title = molecule.residues[0].name if molecule.residues[0].name.strip() else "UNK" + if title in nameset: + orig = title + sfx = 2 + while title in nameset: + title = f'{orig:s}{sfx:d}' + sfx += 1 + else: + title = f'system{sysnum}' + sysnum += 1 + names.append(title) + nameset.add(title) + GromacsTopologyFile._write_molecule(molecule, _molfile, title, params, parameters == 'inline') + if not itp : + # System + dest.write('[ system ]\n; Name\n') + if self.title: + dest.write(self.title) + else: + dest.write('Generic title') # pragma: no cover + dest.write('\n\n') + # Molecules + dest.write('[ molecules ]\n; Compound #mols\n') + total_mols = sum(len(m[1]) for m in new_molecules) + i = 0 + while i < total_mols: + for j, (molecule, lst) in enumerate(new_molecules): + if i in lst: + break + else: + raise AssertionError(f'Could not find molecule {i} in list') + ii = i + while ii < total_mols and ii in lst: + ii += 1 + dest.write(f'{names[j]:<15s} {ii - i:6d}\n') + i = ii + finally: + if own_handle: + dest.close() + if own_parfile_handle: + parfile.close() + if own_molfile_handle: + _molfile.close() + + #=================================================== + + @staticmethod + def _write_molecule(struct, dest, title, params, writeparams): + assert title.strip(), "title must not be whitespace only!" + dest.write('\n[ moleculetype ]\n; Name nrexcl\n') + dest.write(f'{title} {struct.nrexcl}\n\n') + dest.write('[ atoms ]\n') + dest.write('; nr type resnr residue atom cgnr ' + 'charge mass typeB chargeB massB\n') + runchg = 0 + for residue in struct.residues: + dest.write('; residue %4d %s rtp %s q %.1f\n' % + (residue.idx+1, residue.name, residue.name, sum(a.charge for a in residue))) + for atom in residue: + runchg += atom.charge + dest.write('%5d %10s %6d %6s %6s %6d %10.8f %10.6f ; ' + 'qtot %.6f\n' % (atom.idx+1, atom.type, + residue.idx+1, residue.name, atom.name, + atom.idx+1, atom.charge, atom.mass, runchg)) + dest.write('\n') + # Do valence terms now + EPs = [a for a in struct.atoms if isinstance(a, ExtraPoint)] + settle = False + if len(struct.atoms) - len(EPs) == 3: + try: + oxy, = (a for a in struct.atoms if a.atomic_number == 8) + hyd1, hyd2 = (a for a in struct.atoms if a.atomic_number == 1) + settle = True + except ValueError: + pass + if struct.bonds: + conv = (u.kilocalorie_per_mole/u.angstrom**2).conversion_factor_to( + u.kilojoule_per_mole/u.nanometer**2)*2 + if settle: + dest.write('#ifdef FLEXIBLE\n\n') + dest.write('[ bonds ]\n') + dest.write(';%6s %6s %5s %10s %10s %10s %10s\n' % ('ai', 'aj', + 'funct', 'c0', 'c1', 'c2', 'c3')) + for bond in struct.bonds: + if (isinstance(bond.atom1, ExtraPoint) or isinstance(bond.atom2, ExtraPoint)): + continue # pragma: no cover + dest.write('%7d %6d %5d' % (bond.atom1.idx+1, bond.atom2.idx+1, bond.funct)) + if bond.type is None: + dest.write('\n') + continue # pragma: no cover + key = (_gettype(bond.atom1), _gettype(bond.atom2)) + if writeparams or key not in params.bond_types or \ + bond.type != params.bond_types[key]: + dest.write(' %.5f %f' % (bond.type.req/10, bond.type.k*conv)) + dest.write('\n') + dest.write('\n') + # Do the pair-exceptions + if struct.adjusts: + dest.write('[ pairs ]\n') + dest.write(';%6s %6s %5s %10s %10s %10s %10s\n' % ('ai', 'aj', + 'funct', 'c0', 'c1', 'c2', 'c3')) + econv = u.kilocalories.conversion_factor_to(u.kilojoules) + lconv = u.angstroms.conversion_factor_to(u.nanometer) + for adjust in struct.adjusts: + key = (_gettype(adjust.atom1), _gettype(adjust.atom2)) + dest.write('%7d %6d %5d' % (adjust.atom1.idx+1, adjust.atom2.idx+1, adjust.funct)) + if struct.defaults.gen_pairs == 'no' and (writeparams or + key not in params.pair_types or + adjust.type != params.pair_types[key]) and adjust.type is not None: + dest.write(' %.9f %.9f' % (adjust.type.sigma*lconv, adjust.type.epsilon*econv)) + dest.write('\n') + dest.write('\n') + elif struct.dihedrals: + dest.write('[ pairs ]\n') + dest.write(';%6s %6s %5s %10s %10s %10s %10s\n' % ('ai', 'aj', + 'funct', 'c0', 'c1', 'c2', 'c3')) + # Get the 1-4 pairs from the dihedral list + struct.update_dihedral_exclusions() + econv = u.kilocalories.conversion_factor_to(u.kilojoules) + lconv = u.angstroms.conversion_factor_to(u.nanometer) + for dihed in struct.dihedrals: + if dihed.ignore_end or dihed.improper: continue + a1, a2 = dihed.atom1, dihed.atom4 + if a1 in a2.bond_partners or a1 in a2.angle_partners: + continue # pragma: no cover + dest.write('%7d %6d %5d' % (a1.idx+1, a2.idx+1, 1)) + if struct.defaults.gen_pairs == 'no': + dest.write(' %.9f %.9f' % (0.5*(a1.sigma_14+a2.sigma_14)*lconv, + math.sqrt(a1.epsilon_14*a2.epsilon_14)*econv)) + dest.write('\n') + dest.write('\n') + # Angles + if struct.angles: + conv = (u.kilocalorie_per_mole/u.radian**2).conversion_factor_to( + u.kilojoule_per_mole/u.radian**2)*2 + conv2 = (u.kilocalorie_per_mole/u.angstrom**2).conversion_factor_to( + u.kilojoule_per_mole/u.nanometer**2)*2 + dest.write('[ angles ]\n') + dest.write(';%6s %6s %6s %5s %10s %10s %10s %10s\n' % + ('ai', 'aj', 'ak', 'funct', 'c0', 'c1', 'c2', 'c3')) + for angle in struct.angles: + dest.write('%7d %6d %6d %5d' % (angle.atom1.idx+1, angle.atom2.idx+1, + angle.atom3.idx+1, angle.funct)) + if angle.type is None: + dest.write('\n') + continue + key = (_gettype(angle.atom1), _gettype(angle.atom2), _gettype(angle.atom3)) + param_equal = params.angle_types.get(key) == angle.type + if angle.funct == 5: + # Find the Urey-Bradley term, if it exists + for ub in struct.urey_bradleys: + if angle.atom1 in ub and angle.atom3 in ub: + ubtype = ub.type + break + else: + ubtype = NoUreyBradley + param_equal = param_equal and params.urey_bradley_types.get(key) == ubtype + if writeparams or not param_equal: + dest.write(' %.7f %f' % (angle.type.theteq, angle.type.k*conv)) + if angle.funct == 5: + dest.write(' %.7f %f' % (ubtype.req/10, ubtype.k*conv2)) + dest.write('\n') + dest.write('\n') + # Dihedrals + if struct.dihedrals: + dest.write('[ dihedrals ]\n') + dest.write((';%6s %6s %6s %6s %5s'+' %10s'*6) % ('ai', 'aj', + 'ak', 'al', 'funct', 'c0', 'c1', 'c2', 'c3', 'c4', 'c5')) + dest.write('\n') + conv = u.kilocalories.conversion_factor_to(u.kilojoules) + for dihed in struct.dihedrals: + dest.write('%7d %6d %6d %6d %5d' % (dihed.atom1.idx+1, dihed.atom2.idx+1, + dihed.atom3.idx+1, dihed.atom4.idx+1, dihed.funct)) + if dihed.type is None: + dest.write('\n') + continue + if dihed.improper: + typedict = params.improper_periodic_types + else: + typedict = params.dihedral_types + key = (_gettype(dihed.atom1), _gettype(dihed.atom2), + _gettype(dihed.atom3), _gettype(dihed.atom4)) + if writeparams or key not in typedict or \ + _diff_diheds(dihed.type, typedict[key]): + if isinstance(dihed.type, DihedralTypeList): + dest.write(' %.6f %.6f %d' % (dihed.type[0].phase, + dihed.type[0].phi_k*conv, int(dihed.type[0].per))) + for dt in dihed.type[1:]: + dest.write('\n%7d %6d %6d %6d %5d %.5f %.7f %d' % + (dihed.atom1.idx+1, dihed.atom2.idx+1, + dihed.atom3.idx+1, dihed.atom4.idx+1, + dihed.funct, dt.phase, dt.phi_k*conv, int(dt.per))) + else: + dest.write(' %.7f %.7f %d' % (dihed.type.phase, + dihed.type.phi_k*conv, int(dihed.type.per))) + dest.write('\n') + dest.write('\n') + # RB-torsions + if struct.rb_torsions: + dest.write('[ dihedrals ]\n') + dest.write((';%6s %6s %6s %6s %5s'+' %10s'*6) % ('ai', 'aj', + 'ak', 'al', 'funct', 'c0', 'c1', 'c2', 'c3', + 'c4', 'c5')) + dest.write('\n') + conv = u.kilocalories.conversion_factor_to(u.kilojoules) + paramfmt = ' %12.7f %12.7f %12.7f %12.7f %12.7f %12.7f' + for dihed in struct.rb_torsions: + dest.write('%7d %6d %6d %6d %5d' % (dihed.atom1.idx+1, + dihed.atom2.idx+1, dihed.atom3.idx+1, + dihed.atom4.idx+1, dihed.funct)) + if dihed.type is None: + dest.write('\n') + continue + key = (_gettype(dihed.atom1), _gettype(dihed.atom2), + _gettype(dihed.atom3), _gettype(dihed.atom4)) + if writeparams or key not in params.rb_torsion_types or \ + params.rb_torsion_types[key] != dihed.type: + dest.write(paramfmt % ( + dihed.type.c0*conv, dihed.type.c1*conv, dihed.type.c2*conv, + dihed.type.c3*conv, dihed.type.c4*conv, dihed.type.c5*conv + )) + dest.write('\n') + dest.write('\n') + # Impropers + if struct.impropers: + dest.write('[ dihedrals ]\n') + dest.write((';%6s %6s %6s %6s %5s'+' %10s'*4) % ('ai', 'aj', + 'ak', 'al', 'funct', 'c0', 'c1', 'c2', 'c3')) + dest.write('\n') + conv = u.kilocalories.conversion_factor_to(u.kilojoules) * 2 + for dihed in struct.impropers: + dest.write('%7d %6d %6d %6d %5d' % (dihed.atom1.idx+1, + dihed.atom2.idx+1, dihed.atom3.idx+1, + dihed.atom4.idx+1, dihed.funct)) + if dihed.type is None: + dest.write('\n') + continue + # BUGBUG: We always write improper types since we don't + # currently store the correct ordering of the types in the + # improper section + dest.write(' %12.7f %12.7f\n' % (dihed.type.psi_eq, dihed.type.psi_k*conv)) + dest.write('\n') + # Cmaps + if struct.cmaps: + dest.write('[ cmap ]\n') + dest.write(';%6s %6s %6s %6s %6s %5s\n' % ('ai', 'aj', 'ak', + 'al', 'am', 'funct')) + for cmap in struct.cmaps: + dest.write('%7d %6d %6d %6d %6d %5d\n' % (cmap.atom1.idx+1, + cmap.atom2.idx+1, cmap.atom3.idx+1, + cmap.atom4.idx+1, cmap.atom5.idx+1, cmap.funct)) + # See if this is a solvent molecule with 3 or fewer particles that can + # be SETTLEd + if settle: + dest.write('\n#else\n\n') + dest.write('[ settles ]\n') + dest.write('; i funct doh dhh\n') + for b in oxy.bonds: + if hyd1 in b: + # Use default values for TIPnP if no type exists + doh = 0.09572 if b.type is None else b.type.req / 10 + break + for b in hyd1.bonds: + if hyd2 in b: + # Use default values for TIPnP if no type exists + dhh = 0.15139 if b.type is None else b.type.req / 10 + break + else: + for a in oxy.angles: + if hyd1 in a and hyd2 in a: + theteq = a.type.theteq * DEG_TO_RAD + dhh = math.sqrt(2*doh*doh - 2*doh*doh*math.cos(theteq)) + break + else: + raise GromacsError('Cannot determine SETTLE geometry') # pragma: no cover + dest.write('1 1 %.8f %.8f\n\n#endif\n\n' % (doh, dhh)) + # Virtual sites + if EPs: + ftypes = set(type(a.frame_type) for a in EPs) + for ftype in ftypes: + if ftype is TwoParticleExtraPointFrame: + dest.write('[ virtual_sites2 ]\n') + dest.write('; Site from funct a\n') + for EP in EPs: + if not isinstance(EP.frame_type, ftype): continue + a1, a2 = EP.frame_type.get_atoms() + dest.write('%-5d %-4d %-4d %-4d %.6f\n' % + (EP.idx+1, a1.idx+1, a2.idx+1, 1, + EP.frame_type.get_weights()[0])) + dest.write('\n') + elif ftype in (ThreeParticleExtraPointFrame, + OutOfPlaneExtraPointFrame): + dest.write('[ virtual_sites3 ]\n') + dest.write('; Site from funct\n') + for EP in EPs: + if isinstance(EP.frame_type, + ThreeParticleExtraPointFrame): + a1, a2, a3 = EP.frame_type.get_atoms() + junk, w1, w2 = EP.frame_type.get_weights() + dest.write('%-5d %-4d %-4d %-4d %-4d %.6f %.6f\n' + % (EP.idx+1, a1.idx+1, a2.idx+1, a3.idx+1, 1, w1, w2)) + elif isinstance(EP.frame_type, + OutOfPlaneExtraPointFrame): + a1, a2, a3 = EP.frame_type.get_atoms() + w1, w2, w3 = EP.frame_type.get_weights() + dest.write('%-5d %-4d %-4d %-4d %-4d %.6f %.6f %.6f\n' % + (EP.idx+1, a1.idx+1, a2.idx+1, a3.idx+1, w1, w2, w3)) + dest.write('\n') + # Do we need to list exclusions for systems with EPs? + if EPs or settle: + dest.write('[ exclusions ]\n') + for i, atom in enumerate(struct.atoms): + dest.write('%d' % (i+1)) + for a in atom.bond_partners: + dest.write(' %d' % (a.idx+1)) + for a in atom.angle_partners: + dest.write(' %d' % (a.idx+1)) + dest.write('\n') + dest.write('\n') + + #=================================================== + + def __getstate__(self): + d = Structure.__getstate__(self) + d['parameterset'] = self.parameterset + d['defaults'] = self.defaults + return d + + def __setstate__(self, d): + Structure.__setstate__(self, d) + self.parameterset = d['parameterset'] + self.defaults = d['defaults'] + +def _any_atoms_farther_than(structure, limit=3): + """ + This function checks to see if there are any atom pairs farther away in the + bond graph than the desired limit + + Parameters + ---------- + structure : :class:`Structure` + The structure to search through + limit : int, optional + The most number of bonds away to check for. Default is 3 + + Returns + ------- + within : bool + True if any atoms are *more* than ``limit`` bonds away from any other + atom + """ + import sys + if len(structure.atoms) <= limit + 1: return False + sys.setrecursionlimit(max(sys.getrecursionlimit(), limit+1)) + for atom in structure.atoms: + for atom in structure.atoms: atom.marked = limit + 1 + _mark_graph(atom, 0) + if any((atom.marked > limit for atom in structure.atoms)): + return True + return False + +def _mark_graph(atom, num): + """ Marks all atoms in the graph listing the minimum number of bonds each + atom is away from the current atom + + Parameters + ---------- + atom : :class:`Atom` + The current atom to evaluate in the bond graph + num : int + The current depth in our search + limit : int + The maximum depth we want to search + """ + atom.marked = num + for a in atom.bond_partners: + if a.marked <= num: continue + _mark_graph(a, num+1) + +def _diff_diheds(dt1, dt2): + """ Determine if 2 dihedrals are *really* different. dt1 can either be a + DihedralType or a DihedralTypeList or dt1 can be a DihedralType and dt2 can + be a DihedralTypeList. This returns True if dt1 == dt2 *or* dt1 is equal to + the only element of dt2 + """ + if type(dt1) is type(dt2) and dt1 == dt2: + return False + if isinstance(dt2, DihedralTypeList) and isinstance(dt1, DihedralType): + if len(dt2) == 1 and dt2[0] == dt1: return False + return True + +def _gettype(atom): + if atom.atom_type not in (None, UnassignedAtomType): + return atom.atom_type.bond_type + return atom.type diff --git a/pmx/workflow/parmed/modeller/__init__.py b/pmx/workflow/parmed/modeller/__init__.py new file mode 100644 index 00000000..a283d745 --- /dev/null +++ b/pmx/workflow/parmed/modeller/__init__.py @@ -0,0 +1,11 @@ +""" +This package contains functionality necessary to carry out basic molecular +modelling tasks. +""" +__all__ = ['AmberOFFLibrary', 'StandardBiomolecularResidues'] + +from .residue import * +from ..amber.offlib import AmberOFFLibrary +from .standardtemplates import StandardBiomolecularResidues + +__all__ += residue.__all__ diff --git a/pmx/workflow/parmed/modeller/data/standard_residues.lib b/pmx/workflow/parmed/modeller/data/standard_residues.lib new file mode 100644 index 00000000..864cd87b --- /dev/null +++ b/pmx/workflow/parmed/modeller/data/standard_residues.lib @@ -0,0 +1,5244 @@ +!!index array str + "A" + "ACE" + "ALA" + "ARG" + "ASN" + "ASP" + "C" + "CA" + "CU" + "CYS" + "DA" + "DC" + "DG" + "DT" + "F" + "FE" + "G" + "GLN" + "GLU" + "GLY" + "HIS" + "HOH" + "HYP" + "ILE" + "K" + "LEU" + "LI" + "LYS" + "MET" + "MG" + "MN" + "NA" + "NI" + "NME" + "PHE" + "PRO" + "SER" + "SOL" + "THR" + "TIP3" + "TRP" + "TYR" + "U" + "VAL" + "WAT" +!entry.A.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "P" "P" 0 1 131072 1 15 1.166200 + "OP1" "O2" 0 1 131072 2 8 -0.776000 + "OP2" "O2" 0 1 131072 3 8 -0.776000 + "O5'" "OS" 0 1 131072 4 8 -0.498900 + "C5'" "CI" 0 1 131072 5 6 0.055800 + "H5'" "H1" 0 1 131072 6 1 0.067900 + "H5''" "H1" 0 1 131072 7 1 0.067900 + "C4'" "CT" 0 1 131072 8 6 0.106500 + "H4'" "H1" 0 1 131072 9 1 0.117400 + "O4'" "OS" 0 1 131072 10 8 -0.354800 + "C1'" "CT" 0 1 131072 11 6 0.039400 + "H1'" "H2" 0 1 131072 12 1 0.200700 + "N9" "N*" 0 1 131072 13 7 -0.025100 + "C8" "C5" 0 1 131072 14 6 0.200600 + "H8" "H5" 0 1 131072 15 1 0.155300 + "N7" "NB" 0 1 131072 16 7 -0.607300 + "C5" "CB" 0 1 131072 17 6 0.051500 + "C6" "CA" 0 1 131072 18 6 0.700900 + "N6" "N2" 0 1 131072 19 7 -0.901900 + "H61" "H" 0 1 131072 20 1 0.411500 + "H62" "H" 0 1 131072 21 1 0.411500 + "N1" "NC" 0 1 131072 22 7 -0.761500 + "C2" "CQ" 0 1 131072 23 6 0.587500 + "H2" "H5" 0 1 131072 24 1 0.047300 + "N3" "NC" 0 1 131072 25 7 -0.699700 + "C4" "CB" 0 1 131072 26 6 0.305300 + "C3'" "CT" 0 1 131072 27 6 0.202200 + "H3'" "H1" 0 1 131072 28 1 0.061500 + "C2'" "CT" 0 1 131072 29 6 0.067000 + "H2'" "H1" 0 1 131072 30 1 0.097200 + "O2'" "OH" 0 1 131072 31 8 -0.613900 + "HO2'" "HO" 0 1 131072 32 1 0.418600 + "O3'" "OS" 0 1 131072 33 8 -0.524600 +!entry.A.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "P" "P" 0 -1 0.0 + "OP1" "O2" 0 -1 0.0 + "OP2" "O2" 0 -1 0.0 + "O5'" "OS" 0 -1 0.0 + "C5'" "CI" 0 -1 0.0 + "H5'" "H1" 0 -1 0.0 + "H5''" "H1" 0 -1 0.0 + "C4'" "CT" 0 -1 0.0 + "H4'" "H1" 0 -1 0.0 + "O4'" "OS" 0 -1 0.0 + "C1'" "CT" 0 -1 0.0 + "H1'" "H2" 0 -1 0.0 + "N9" "N*" 0 -1 0.0 + "C8" "C5" 0 -1 0.0 + "H8" "H5" 0 -1 0.0 + "N7" "NB" 0 -1 0.0 + "C5" "CB" 0 -1 0.0 + "C6" "CA" 0 -1 0.0 + "N6" "N2" 0 -1 0.0 + "H61" "H" 0 -1 0.0 + "H62" "H" 0 -1 0.0 + "N1" "NC" 0 -1 0.0 + "C2" "CQ" 0 -1 0.0 + "H2" "H5" 0 -1 0.0 + "N3" "NC" 0 -1 0.0 + "C4" "CB" 0 -1 0.0 + "C3'" "CT" 0 -1 0.0 + "H3'" "H1" 0 -1 0.0 + "C2'" "CT" 0 -1 0.0 + "H2'" "H1" 0 -1 0.0 + "O2'" "OH" 0 -1 0.0 + "HO2'" "HO" 0 -1 0.0 + "O3'" "OS" 0 -1 0.0 +!entry.A.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.A.unit.childsequence single int + 2 +!entry.A.unit.connect array int + 1 + 33 +!entry.A.unit.connectivity table int atom1x int atom2x int flags + 1 2 1 + 1 3 1 + 1 4 1 + 4 5 1 + 5 6 1 + 5 7 1 + 5 8 1 + 8 9 1 + 8 10 1 + 8 27 1 + 10 11 1 + 11 12 1 + 11 13 1 + 11 29 1 + 13 14 1 + 13 26 1 + 14 15 1 + 14 16 1 + 16 17 1 + 17 18 1 + 17 26 1 + 18 19 1 + 18 22 1 + 19 20 1 + 19 21 1 + 22 23 1 + 23 24 1 + 23 25 1 + 25 26 1 + 27 28 1 + 27 29 1 + 27 33 1 + 29 30 1 + 29 31 1 + 31 32 1 +!entry.A.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 + "R" 1 "A" 2 + "R" 1 "A" 3 + "R" 1 "A" 4 + "R" 1 "A" 5 + "R" 1 "A" 6 + "R" 1 "A" 7 + "R" 1 "A" 8 + "R" 1 "A" 9 + "R" 1 "A" 10 + "R" 1 "A" 11 + "R" 1 "A" 12 + "R" 1 "A" 13 + "R" 1 "A" 14 + "R" 1 "A" 15 + "R" 1 "A" 16 + "R" 1 "A" 17 + "R" 1 "A" 18 + "R" 1 "A" 19 + "R" 1 "A" 20 + "R" 1 "A" 21 + "R" 1 "A" 22 + "R" 1 "A" 23 + "R" 1 "A" 24 + "R" 1 "A" 25 + "R" 1 "A" 26 + "R" 1 "A" 27 + "R" 1 "A" 28 + "R" 1 "A" 29 + "R" 1 "A" 30 + "R" 1 "A" 31 + "R" 1 "A" 32 + "R" 1 "A" 33 +!entry.A.unit.name single str + "A" +!entry.A.unit.positions table dbl x dbl y dbl z + 2.31449 1.77667 -0.478436 + 1.93341 3.07337 -1.08155 + 3.15655 0.871841 -1.29245 + 2.99437 2.14248 0.922974 + 2.15604 2.55184 2.01989 + 1.45269 1.75384 2.2578 + 1.60497 3.45003 1.74118 + 3.00825 2.84792 3.24319 + 2.44712 3.47737 3.93385 + 3.22812 1.60517 3.97723 + 4.55861 1.15095 3.77755 + 5.072 1.09528 4.73746 + 4.53889 -0.297039 3.31566 + 4.33675 -0.846471 2.07705 + 4.1524 -0.207228 1.22629 + 4.45287 -2.14097 2.04908 + 4.75406 -2.48004 3.36301 + 4.998 -3.7082 3.98922 + 4.97679 -4.88349 3.34592 + 5.16072 -5.73881 3.8506 + 4.77686 -4.91403 2.35638 + 5.2653 -3.68934 5.30215 + 5.28498 -2.51987 5.93529 + 5.49828 -2.48215 6.99334 + 5.07269 -1.30851 5.45578 + 4.80614 -1.35635 4.13322 + 4.41561 3.37783 2.96131 + 4.41207 3.94149 2.02837 + 5.23044 2.0957 2.77942 + 5.66821 2.08131 1.7813 + 6.27298 2.04994 3.75714 + 6.75928 1.23523 3.61099 + 5.0299 4.12865 3.99828 +!entry.A.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 33 0 0 0 0 +!entry.A.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "A" 1 34 1 "n" 13 +!entry.A.unit.residuesPdbSequenceNumber array int + 0 +!entry.A.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.A.unit.velocities table dbl x dbl y dbl z + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 +!entry.ACE.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "HH31" "HC" 0 1 131072 1 1 0.112300 + "CH3" "CT" 0 1 131072 2 6 -0.366200 + "HH32" "HC" 0 1 131072 3 1 0.112300 + "HH33" "HC" 0 1 131072 4 1 0.112300 + "C" "C" 0 1 131072 5 6 0.597200 + "O" "O" 0 1 131072 6 8 -0.567900 +!entry.ACE.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "HH31" "HC" 0 -1 0.0 + "CH3" "CT" 0 -1 0.0 + "HH32" "HC" 0 -1 0.0 + "HH33" "HC" 0 -1 0.0 + "C" "C" 0 -1 0.0 + "O" "O" 0 -1 0.0 +!entry.ACE.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.ACE.unit.childsequence single int + 2 +!entry.ACE.unit.connect array int + 0 + 5 +!entry.ACE.unit.connectivity table int atom1x int atom2x int flags + 1 2 1 + 2 3 1 + 2 4 1 + 2 5 1 + 5 6 1 +!entry.ACE.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 + "R" 1 "A" 2 + "R" 1 "A" 3 + "R" 1 "A" 4 + "R" 1 "A" 5 + "R" 1 "A" 6 +!entry.ACE.unit.name single str + "ACE" +!entry.ACE.unit.positions table dbl x dbl y dbl z + 2 1 -1.34641e-06 + 2 2.09 1.21177e-07 + 1.48626 2.45385 0.889824 + 1.48626 2.45385 -0.88982 + 3.42742 2.64079 -2.98101e-06 + 4.39058 1.87741 -6.60245e-06 +!entry.ACE.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 5 0 0 0 0 +!entry.ACE.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "ACE" 1 7 1 "p" 5 +!entry.ACE.unit.residuesPdbSequenceNumber array int + 0 +!entry.ACE.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.ACE.unit.velocities table dbl x dbl y dbl z + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 +!entry.ALA.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "N" "N" 0 1 131072 1 7 -0.415700 + "H" "H" 0 1 131072 2 1 0.271900 + "CA" "CX" 0 1 131072 3 6 0.033700 + "HA" "H1" 0 1 131072 4 1 0.082300 + "CB" "CT" 0 1 131072 5 6 -0.182500 + "HB1" "HC" 0 1 131072 6 1 0.060300 + "HB2" "HC" 0 1 131072 7 1 0.060300 + "HB3" "HC" 0 1 131072 8 1 0.060300 + "C" "C" 0 1 131072 9 6 0.597300 + "O" "O" 0 1 131072 10 8 -0.567900 +!entry.ALA.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "N" "N" 0 -1 0.0 + "H" "H" 0 -1 0.0 + "CA" "CX" 0 -1 0.0 + "HA" "H1" 0 -1 0.0 + "CB" "CT" 0 -1 0.0 + "HB1" "HC" 0 -1 0.0 + "HB2" "HC" 0 -1 0.0 + "HB3" "HC" 0 -1 0.0 + "C" "C" 0 -1 0.0 + "O" "O" 0 -1 0.0 +!entry.ALA.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.ALA.unit.childsequence single int + 2 +!entry.ALA.unit.connect array int + 1 + 9 +!entry.ALA.unit.connectivity table int atom1x int atom2x int flags + 1 2 1 + 1 3 1 + 3 4 1 + 3 5 1 + 3 9 1 + 5 6 1 + 5 7 1 + 5 8 1 + 9 10 1 +!entry.ALA.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 + "R" 1 "A" 2 + "R" 1 "A" 3 + "R" 1 "A" 4 + "R" 1 "A" 5 + "R" 1 "A" 6 + "R" 1 "A" 7 + "R" 1 "A" 8 + "R" 1 "A" 9 + "R" 1 "A" 10 +!entry.ALA.unit.name single str + "ALA" +!entry.ALA.unit.positions table dbl x dbl y dbl z + 3.32577 1.54791 -1.6072e-06 + 3.90941 0.723611 -2.73988e-06 + 3.97005 2.84579 -1.31116e-07 + 3.67166 3.40013 -0.88982 + 3.57696 3.65384 1.23214 + 3.87748 3.11579 2.1312 + 4.07506 4.62302 1.20579 + 2.497 3.80107 1.24138 + 5.48554 2.70521 -4.39876e-06 + 6.00882 1.59318 -8.44977e-06 +!entry.ALA.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 9 0 0 0 0 +!entry.ALA.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "ALA" 1 11 1 "p" 3 +!entry.ALA.unit.residuesPdbSequenceNumber array int + 0 +!entry.ALA.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.ALA.unit.velocities table dbl x dbl y dbl z + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 +!entry.ARG.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "N" "N" 0 1 131072 1 7 -0.347900 + "H" "H" 0 1 131072 2 1 0.274700 + "CA" "CX" 0 1 131072 3 6 -0.263700 + "HA" "H1" 0 1 131072 4 1 0.156000 + "CB" "C8" 0 1 131072 5 6 -0.000700 + "HB2" "HC" 0 1 131072 6 1 0.032700 + "HB3" "HC" 0 1 131072 7 1 0.032700 + "CG" "C8" 0 1 131072 8 6 0.039000 + "HG2" "HC" 0 1 131072 9 1 0.028500 + "HG3" "HC" 0 1 131072 10 1 0.028500 + "CD" "C8" 0 1 131072 11 6 0.048600 + "HD2" "H1" 0 1 131072 12 1 0.068700 + "HD3" "H1" 0 1 131072 13 1 0.068700 + "NE" "N2" 0 1 131072 14 7 -0.529500 + "HE" "H" 0 1 131072 15 1 0.345600 + "CZ" "CA" 0 1 131072 16 6 0.807600 + "NH1" "N2" 0 1 131072 17 7 -0.862700 + "HH11" "H" 0 1 131072 18 1 0.447800 + "HH12" "H" 0 1 131072 19 1 0.447800 + "NH2" "N2" 0 1 131072 20 7 -0.862700 + "HH21" "H" 0 1 131072 21 1 0.447800 + "HH22" "H" 0 1 131072 22 1 0.447800 + "C" "C" 0 1 131072 23 6 0.734100 + "O" "O" 0 1 131072 24 8 -0.589400 +!entry.ARG.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "N" "N" 0 -1 0.0 + "H" "H" 0 -1 0.0 + "CA" "CX" 0 -1 0.0 + "HA" "H1" 0 -1 0.0 + "CB" "C8" 0 -1 0.0 + "HB2" "HC" 0 -1 0.0 + "HB3" "HC" 0 -1 0.0 + "CG" "C8" 0 -1 0.0 + "HG2" "HC" 0 -1 0.0 + "HG3" "HC" 0 -1 0.0 + "CD" "C8" 0 -1 0.0 + "HD2" "H1" 0 -1 0.0 + "HD3" "H1" 0 -1 0.0 + "NE" "N2" 0 -1 0.0 + "HE" "H" 0 -1 0.0 + "CZ" "CA" 0 -1 0.0 + "NH1" "N2" 0 -1 0.0 + "HH11" "H" 0 -1 0.0 + "HH12" "H" 0 -1 0.0 + "NH2" "N2" 0 -1 0.0 + "HH21" "H" 0 -1 0.0 + "HH22" "H" 0 -1 0.0 + "C" "C" 0 -1 0.0 + "O" "O" 0 -1 0.0 +!entry.ARG.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.ARG.unit.childsequence single int + 2 +!entry.ARG.unit.connect array int + 1 + 23 +!entry.ARG.unit.connectivity table int atom1x int atom2x int flags + 1 2 1 + 1 3 1 + 3 4 1 + 3 5 1 + 3 23 1 + 5 6 1 + 5 7 1 + 5 8 1 + 8 9 1 + 8 10 1 + 8 11 1 + 11 12 1 + 11 13 1 + 11 14 1 + 14 15 1 + 14 16 1 + 16 17 1 + 16 20 1 + 17 18 1 + 17 19 1 + 20 21 1 + 20 22 1 + 23 24 1 +!entry.ARG.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 + "R" 1 "A" 2 + "R" 1 "A" 3 + "R" 1 "A" 4 + "R" 1 "A" 5 + "R" 1 "A" 6 + "R" 1 "A" 7 + "R" 1 "A" 8 + "R" 1 "A" 9 + "R" 1 "A" 10 + "R" 1 "A" 11 + "R" 1 "A" 12 + "R" 1 "A" 13 + "R" 1 "A" 14 + "R" 1 "A" 15 + "R" 1 "A" 16 + "R" 1 "A" 17 + "R" 1 "A" 18 + "R" 1 "A" 19 + "R" 1 "A" 20 + "R" 1 "A" 21 + "R" 1 "A" 22 + "R" 1 "A" 23 + "R" 1 "A" 24 +!entry.ARG.unit.name single str + "ARG" +!entry.ARG.unit.positions table dbl x dbl y dbl z + 3.32577 1.54791 -1.6072e-06 + 3.90941 0.723611 -2.73988e-06 + 3.97005 2.84579 -1.31116e-07 + 3.67166 3.40013 -0.88982 + 3.57696 3.65384 1.23214 + 2.497 3.80107 1.24138 + 3.87748 3.11579 2.1312 + 4.27419 5.0096 1.19458 + 5.35427 4.86318 1.18579 + 3.97378 5.54846 0.295972 + 3.8811 5.81764 2.42672 + 2.80113 5.96488 2.43596 + 4.18163 5.2796 3.32577 + 4.54032 7.14272 2.42448 + 5.15181 7.37549 1.65507 + 4.36428 8.04099 3.38938 + 3.57503 7.80761 4.43413 + 3.08895 6.92542 4.50885 + 3.46537 8.51363 5.148 + 5.00625 9.20129 3.28699 + 5.60485 9.37533 2.49233 + 4.89222 9.90305 4.00437 + 5.48554 2.70521 -4.39876e-06 + 6.00882 1.59318 -8.44977e-06 +!entry.ARG.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 23 0 0 0 0 +!entry.ARG.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "ARG" 1 25 1 "p" 8 +!entry.ARG.unit.residuesPdbSequenceNumber array int + 0 +!entry.ARG.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.ARG.unit.velocities table dbl x dbl y dbl z + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 +!entry.ASN.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "N" "N" 0 1 131072 1 7 -0.415700 + "H" "H" 0 1 131072 2 1 0.271900 + "CA" "CX" 0 1 131072 3 6 0.014300 + "HA" "H1" 0 1 131072 4 1 0.104800 + "CB" "2C" 0 1 131072 5 6 -0.204100 + "HB2" "HC" 0 1 131072 6 1 0.079700 + "HB3" "HC" 0 1 131072 7 1 0.079700 + "CG" "C" 0 1 131072 8 6 0.713000 + "OD1" "O" 0 1 131072 9 8 -0.593100 + "ND2" "N" 0 1 131072 10 7 -0.919100 + "HD21" "H" 0 1 131072 11 1 0.419600 + "HD22" "H" 0 1 131072 12 1 0.419600 + "C" "C" 0 1 131072 13 6 0.597300 + "O" "O" 0 1 131072 14 8 -0.567900 +!entry.ASN.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "N" "N" 0 -1 0.0 + "H" "H" 0 -1 0.0 + "CA" "CX" 0 -1 0.0 + "HA" "H1" 0 -1 0.0 + "CB" "2C" 0 -1 0.0 + "HB2" "HC" 0 -1 0.0 + "HB3" "HC" 0 -1 0.0 + "CG" "C" 0 -1 0.0 + "OD1" "O" 0 -1 0.0 + "ND2" "N" 0 -1 0.0 + "HD21" "H" 0 -1 0.0 + "HD22" "H" 0 -1 0.0 + "C" "C" 0 -1 0.0 + "O" "O" 0 -1 0.0 +!entry.ASN.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.ASN.unit.childsequence single int + 2 +!entry.ASN.unit.connect array int + 1 + 13 +!entry.ASN.unit.connectivity table int atom1x int atom2x int flags + 1 2 1 + 1 3 1 + 3 4 1 + 3 5 1 + 3 13 1 + 5 6 1 + 5 7 1 + 5 8 1 + 8 9 1 + 8 10 1 + 10 11 1 + 10 12 1 + 13 14 1 +!entry.ASN.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 + "R" 1 "A" 2 + "R" 1 "A" 3 + "R" 1 "A" 4 + "R" 1 "A" 5 + "R" 1 "A" 6 + "R" 1 "A" 7 + "R" 1 "A" 8 + "R" 1 "A" 9 + "R" 1 "A" 10 + "R" 1 "A" 11 + "R" 1 "A" 12 + "R" 1 "A" 13 + "R" 1 "A" 14 +!entry.ASN.unit.name single str + "ASN" +!entry.ASN.unit.positions table dbl x dbl y dbl z + 3.32577 1.54791 -1.6072e-06 + 3.90941 0.723611 -2.73988e-06 + 3.97005 2.84579 -1.31116e-07 + 3.67166 3.40013 -0.88982 + 3.57696 3.65384 1.23214 + 2.497 3.80107 1.24138 + 3.87748 3.11579 2.1312 + 4.2537 5.01711 1.23214 + 5.0053 5.34041 0.315072 + 3.98488 5.81791 2.26592 + 4.40801 6.7337 2.31474 + 3.35961 5.5043 2.99446 + 5.48554 2.70521 -4.39876e-06 + 6.00882 1.59318 -8.44977e-06 +!entry.ASN.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 13 0 0 0 0 +!entry.ASN.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "ASN" 1 15 1 "p" 5 +!entry.ASN.unit.residuesPdbSequenceNumber array int + 0 +!entry.ASN.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.ASN.unit.velocities table dbl x dbl y dbl z + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 +!entry.ASP.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "N" "N" 0 1 131072 1 7 -0.415700 + "H" "H" 0 1 131072 2 1 0.271900 + "CA" "CX" 0 1 131072 3 6 0.034100 + "HA" "H1" 0 1 131072 4 1 0.086400 + "CB" "2C" 0 1 131072 5 6 -0.031600 + "HB2" "HC" 0 1 131072 6 1 0.048800 + "HB3" "HC" 0 1 131072 7 1 0.048800 + "CG" "C" 0 1 131072 8 6 0.646200 + "OD1" "O" 0 1 131072 9 8 -0.555400 + "OD2" "OH" 0 1 131072 10 8 -0.637600 + "HD2" "HO" 0 1 131072 11 1 0.474700 + "C" "C" 0 1 131072 12 6 0.597300 + "O" "O" 0 1 131072 13 8 -0.567900 +!entry.ASP.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "N" "N" 0 -1 0.0 + "H" "H" 0 -1 0.0 + "CA" "CX" 0 -1 0.0 + "HA" "H1" 0 -1 0.0 + "CB" "2C" 0 -1 0.0 + "HB2" "HC" 0 -1 0.0 + "HB3" "HC" 0 -1 0.0 + "CG" "C" 0 -1 0.0 + "OD1" "O" 0 -1 0.0 + "OD2" "OH" 0 -1 0.0 + "HD2" "HO" 0 -1 0.0 + "C" "C" 0 -1 0.0 + "O" "O" 0 -1 0.0 +!entry.ASP.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.ASP.unit.childsequence single int + 2 +!entry.ASP.unit.connect array int + 1 + 12 +!entry.ASP.unit.connectivity table int atom1x int atom2x int flags + 1 2 1 + 1 3 1 + 3 4 1 + 3 5 1 + 3 12 1 + 5 6 1 + 5 7 1 + 5 8 1 + 8 9 1 + 8 10 1 + 10 11 1 + 12 13 1 +!entry.ASP.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 + "R" 1 "A" 2 + "R" 1 "A" 3 + "R" 1 "A" 4 + "R" 1 "A" 5 + "R" 1 "A" 6 + "R" 1 "A" 7 + "R" 1 "A" 8 + "R" 1 "A" 9 + "R" 1 "A" 10 + "R" 1 "A" 11 + "R" 1 "A" 12 + "R" 1 "A" 13 +!entry.ASP.unit.name single str + "ASP" +!entry.ASP.unit.positions table dbl x dbl y dbl z + 3.32577 1.54791 -1.6072e-06 + 3.90941 0.723611 -2.73988e-06 + 3.97005 2.84579 -1.31116e-07 + 3.67166 3.40013 -0.88982 + 3.57696 3.65384 1.23214 + 2.497 3.80107 1.24138 + 3.87748 3.11579 2.1312 + 4.2751 5.01138 1.19453 + 3.66911 5.95494 0.620011 + 5.40773 5.09188 1.74067 + 5.7429 5.98718 1.65292 + 5.48554 2.70521 -4.39876e-06 + 6.00882 1.59318 -8.44977e-06 +!entry.ASP.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 12 0 0 0 0 +!entry.ASP.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "ASP" 1 14 1 "p" 3 +!entry.ASP.unit.residuesPdbSequenceNumber array int + 0 +!entry.ASP.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.ASP.unit.velocities table dbl x dbl y dbl z + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 +!entry.C.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "P" "P" 0 1 131072 1 15 1.166200 + "OP1" "O2" 0 1 131072 2 8 -0.776000 + "OP2" "O2" 0 1 131072 3 8 -0.776000 + "O5'" "OS" 0 1 131072 4 8 -0.498900 + "C5'" "CI" 0 1 131072 5 6 0.055800 + "H5'" "H1" 0 1 131072 6 1 0.067900 + "H5''" "H1" 0 1 131072 7 1 0.067900 + "C4'" "CT" 0 1 131072 8 6 0.106500 + "H4'" "H1" 0 1 131072 9 1 0.117400 + "O4'" "OS" 0 1 131072 10 8 -0.354800 + "C1'" "CT" 0 1 131072 11 6 0.006600 + "H1'" "H2" 0 1 131072 12 1 0.202900 + "N1" "N*" 0 1 131072 13 7 -0.048400 + "C6" "C4" 0 1 131072 14 6 0.005300 + "H6" "H4" 0 1 131072 15 1 0.195800 + "C5" "C4" 0 1 131072 16 6 -0.521500 + "H5" "HA" 0 1 131072 17 1 0.192800 + "C4" "CA" 0 1 131072 18 6 0.818500 + "N4" "N2" 0 1 131072 19 7 -0.953000 + "H41" "H" 0 1 131072 20 1 0.423400 + "H42" "H" 0 1 131072 21 1 0.423400 + "N3" "NC" 0 1 131072 22 7 -0.758400 + "C2" "C" 0 1 131072 23 6 0.753800 + "O2" "O" 0 1 131072 24 8 -0.625200 + "C3'" "CT" 0 1 131072 25 6 0.202200 + "H3'" "H1" 0 1 131072 26 1 0.061500 + "C2'" "CT" 0 1 131072 27 6 0.067000 + "H2'" "H1" 0 1 131072 28 1 0.097200 + "O2'" "OH" 0 1 131072 29 8 -0.613900 + "HO2'" "HO" 0 1 131072 30 1 0.418600 + "O3'" "OS" 0 1 131072 31 8 -0.524600 +!entry.C.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "P" "P" 0 -1 0.0 + "OP1" "O2" 0 -1 0.0 + "OP2" "O2" 0 -1 0.0 + "O5'" "OS" 0 -1 0.0 + "C5'" "CI" 0 -1 0.0 + "H5'" "H1" 0 -1 0.0 + "H5''" "H1" 0 -1 0.0 + "C4'" "CT" 0 -1 0.0 + "H4'" "H1" 0 -1 0.0 + "O4'" "OS" 0 -1 0.0 + "C1'" "CT" 0 -1 0.0 + "H1'" "H2" 0 -1 0.0 + "N1" "N*" 0 -1 0.0 + "C6" "C4" 0 -1 0.0 + "H6" "H4" 0 -1 0.0 + "C5" "C4" 0 -1 0.0 + "H5" "HA" 0 -1 0.0 + "C4" "CA" 0 -1 0.0 + "N4" "N2" 0 -1 0.0 + "H41" "H" 0 -1 0.0 + "H42" "H" 0 -1 0.0 + "N3" "NC" 0 -1 0.0 + "C2" "C" 0 -1 0.0 + "O2" "O" 0 -1 0.0 + "C3'" "CT" 0 -1 0.0 + "H3'" "H1" 0 -1 0.0 + "C2'" "CT" 0 -1 0.0 + "H2'" "H1" 0 -1 0.0 + "O2'" "OH" 0 -1 0.0 + "HO2'" "HO" 0 -1 0.0 + "O3'" "OS" 0 -1 0.0 +!entry.C.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.C.unit.childsequence single int + 2 +!entry.C.unit.connect array int + 1 + 31 +!entry.C.unit.connectivity table int atom1x int atom2x int flags + 1 2 1 + 1 3 1 + 1 4 1 + 4 5 1 + 5 6 1 + 5 7 1 + 5 8 1 + 8 9 1 + 8 10 1 + 8 25 1 + 10 11 1 + 11 12 1 + 11 13 1 + 11 27 1 + 13 14 1 + 13 23 1 + 14 15 1 + 14 16 1 + 16 17 1 + 16 18 1 + 18 19 1 + 18 22 1 + 19 20 1 + 19 21 1 + 22 23 1 + 23 24 1 + 25 26 1 + 25 27 1 + 25 31 1 + 27 28 1 + 27 29 1 + 29 30 1 +!entry.C.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 + "R" 1 "A" 2 + "R" 1 "A" 3 + "R" 1 "A" 4 + "R" 1 "A" 5 + "R" 1 "A" 6 + "R" 1 "A" 7 + "R" 1 "A" 8 + "R" 1 "A" 9 + "R" 1 "A" 10 + "R" 1 "A" 11 + "R" 1 "A" 12 + "R" 1 "A" 13 + "R" 1 "A" 14 + "R" 1 "A" 15 + "R" 1 "A" 16 + "R" 1 "A" 17 + "R" 1 "A" 18 + "R" 1 "A" 19 + "R" 1 "A" 20 + "R" 1 "A" 21 + "R" 1 "A" 22 + "R" 1 "A" 23 + "R" 1 "A" 24 + "R" 1 "A" 25 + "R" 1 "A" 26 + "R" 1 "A" 27 + "R" 1 "A" 28 + "R" 1 "A" 29 + "R" 1 "A" 30 + "R" 1 "A" 31 +!entry.C.unit.name single str + "C" +!entry.C.unit.positions table dbl x dbl y dbl z + 2.31449 1.77667 -0.478436 + 1.93341 3.07337 -1.08155 + 3.15655 0.871841 -1.29245 + 2.99437 2.14248 0.922974 + 2.15604 2.55184 2.01989 + 1.45269 1.75384 2.2578 + 1.60497 3.45003 1.74118 + 3.00825 2.84792 3.24319 + 2.44712 3.47737 3.93385 + 3.22812 1.60517 3.97723 + 4.55861 1.15095 3.77755 + 5.072 1.09528 4.73746 + 4.50057 -0.267961 3.32653 + 4.2582 -0.582249 2.02573 + 4.10396 0.202767 1.30022 + 4.20566 -1.87808 1.61627 + 4.01025 -2.11143 0.569628 + 4.4162 -2.88083 2.6138 + 4.3789 -4.15892 2.2859 + 4.53246 -4.83639 3.01908 + 4.20177 -4.43444 1.33049 + 4.65265 -2.56446 3.8838 + 4.70124 -1.26231 4.27323 + 4.91896 -0.927584 5.44718 + 4.41561 3.37783 2.96131 + 4.41207 3.94149 2.02837 + 5.23044 2.0957 2.77942 + 5.66821 2.08131 1.7813 + 6.27298 2.04994 3.75714 + 6.75928 1.23523 3.61099 + 5.0299 4.12865 3.99828 +!entry.C.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 31 0 0 0 0 +!entry.C.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "C" 1 32 1 "n" 15 +!entry.C.unit.residuesPdbSequenceNumber array int + 0 +!entry.C.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.C.unit.velocities table dbl x dbl y dbl z + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 +!entry.CA.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "CA" "Ca2+" 0 1 131072 1 20 2.000000 +!entry.CA.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "CA" "Ca2+" 0 -1 0.0 +!entry.CA.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.CA.unit.childsequence single int + 2 +!entry.CA.unit.connect array int + 0 + 0 +!entry.CA.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 +!entry.CA.unit.name single str + "CA" +!entry.CA.unit.positions table dbl x dbl y dbl z + 0 0 0 +!entry.CA.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 1 0 0 0 0 +!entry.CA.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "CA" 1 2 1 "?" 0 +!entry.CA.unit.residuesPdbSequenceNumber array int + 0 +!entry.CA.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.CA.unit.velocities table dbl x dbl y dbl z + 0 0 0 +!entry.CU.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "CU" "Cu2+" 0 1 131072 1 29 2.000000 +!entry.CU.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "CU" "Cu2+" 0 -1 0.0 +!entry.CU.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.CU.unit.childsequence single int + 2 +!entry.CU.unit.connect array int + 0 + 0 +!entry.CU.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 +!entry.CU.unit.name single str + "CU" +!entry.CU.unit.positions table dbl x dbl y dbl z + 0 0 0 +!entry.CU.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 1 0 0 0 0 +!entry.CU.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "CU" 1 2 1 "?" 0 +!entry.CU.unit.residuesPdbSequenceNumber array int + 0 +!entry.CU.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.CU.unit.velocities table dbl x dbl y dbl z + 0 0 0 +!entry.CYS.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "N" "N" 0 1 131072 1 7 -0.415700 + "H" "H" 0 1 131072 2 1 0.271900 + "CA" "CX" 0 1 131072 3 6 0.021300 + "HA" "H1" 0 1 131072 4 1 0.112400 + "CB" "2C" 0 1 131072 5 6 -0.123100 + "HB2" "H1" 0 1 131072 6 1 0.111200 + "HB3" "H1" 0 1 131072 7 1 0.111200 + "SG" "SH" 0 1 131072 8 16 -0.311900 + "HG" "HS" 0 1 131072 9 1 0.193300 + "C" "C" 0 1 131072 10 6 0.597300 + "O" "O" 0 1 131072 11 8 -0.567900 +!entry.CYS.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "N" "N" 0 -1 0.0 + "H" "H" 0 -1 0.0 + "CA" "CX" 0 -1 0.0 + "HA" "H1" 0 -1 0.0 + "CB" "2C" 0 -1 0.0 + "HB2" "H1" 0 -1 0.0 + "HB3" "H1" 0 -1 0.0 + "SG" "SH" 0 -1 0.0 + "HG" "HS" 0 -1 0.0 + "C" "C" 0 -1 0.0 + "O" "O" 0 -1 0.0 +!entry.CYS.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.CYS.unit.childsequence single int + 2 +!entry.CYS.unit.connect array int + 1 + 10 +!entry.CYS.unit.connectivity table int atom1x int atom2x int flags + 1 2 1 + 1 3 1 + 3 4 1 + 3 5 1 + 3 10 1 + 5 6 1 + 5 7 1 + 5 8 1 + 8 9 1 + 10 11 1 +!entry.CYS.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 + "R" 1 "A" 2 + "R" 1 "A" 3 + "R" 1 "A" 4 + "R" 1 "A" 5 + "R" 1 "A" 6 + "R" 1 "A" 7 + "R" 1 "A" 8 + "R" 1 "A" 9 + "R" 1 "A" 10 + "R" 1 "A" 11 +!entry.CYS.unit.name single str + "CYS" +!entry.CYS.unit.positions table dbl x dbl y dbl z + 3.32577 1.54791 -1.6072e-06 + 3.90941 0.723611 -2.73988e-06 + 3.97005 2.84579 -1.31116e-07 + 3.67166 3.40013 -0.88982 + 3.57696 3.65384 1.23214 + 2.497 3.80107 1.24138 + 3.87748 3.11579 2.1312 + 4.30957 5.30352 1.36604 + 3.72539 5.62202 2.51764 + 5.48554 2.70521 -4.39876e-06 + 6.00882 1.59318 -8.44977e-06 +!entry.CYS.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 10 0 0 0 0 +!entry.CYS.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "CYS" 1 12 1 "p" 3 +!entry.CYS.unit.residuesPdbSequenceNumber array int + 0 +!entry.CYS.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.CYS.unit.velocities table dbl x dbl y dbl z + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 +!entry.DA.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "P" "P" 0 1 131072 1 15 1.165900 + "OP1" "O2" 0 1 131072 2 8 -0.776100 + "OP2" "O2" 0 1 131072 3 8 -0.776100 + "O5'" "OS" 0 1 131072 4 8 -0.495400 + "C5'" "CI" 0 1 131072 5 6 -0.006900 + "H5'" "H1" 0 1 131072 6 1 0.075400 + "H5''" "H1" 0 1 131072 7 1 0.075400 + "C4'" "CT" 0 1 131072 8 6 0.162900 + "H4'" "H1" 0 1 131072 9 1 0.117600 + "O4'" "OS" 0 1 131072 10 8 -0.369100 + "C1'" "CT" 0 1 131072 11 6 0.043100 + "H1'" "H2" 0 1 131072 12 1 0.183800 + "N9" "N*" 0 1 131072 13 7 -0.026800 + "C8" "CK" 0 1 131072 14 6 0.160700 + "H8" "H5" 0 1 131072 15 1 0.187700 + "N7" "NB" 0 1 131072 16 7 -0.617500 + "C5" "CB" 0 1 131072 17 6 0.072500 + "C6" "CA" 0 1 131072 18 6 0.689700 + "N6" "N2" 0 1 131072 19 7 -0.912300 + "H61" "H" 0 1 131072 20 1 0.416700 + "H62" "H" 0 1 131072 21 1 0.416700 + "N1" "NC" 0 1 131072 22 7 -0.762400 + "C2" "CQ" 0 1 131072 23 6 0.571600 + "H2" "H5" 0 1 131072 24 1 0.059800 + "N3" "NC" 0 1 131072 25 7 -0.741700 + "C4" "CB" 0 1 131072 26 6 0.380000 + "C3'" "CT" 0 1 131072 27 6 0.071300 + "H3'" "H1" 0 1 131072 28 1 0.098500 + "C2'" "CT" 0 1 131072 29 6 -0.085400 + "H2'" "HC" 0 1 131072 30 1 0.071800 + "H2''" "HC" 0 1 131072 31 1 0.071800 + "O3'" "OS" 0 1 131072 32 8 -0.523200 +!entry.DA.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "P" "P" 0 -1 0.0 + "OP1" "O2" 0 -1 0.0 + "OP2" "O2" 0 -1 0.0 + "O5'" "OS" 0 -1 0.0 + "C5'" "CI" 0 -1 0.0 + "H5'" "H1" 0 -1 0.0 + "H5''" "H1" 0 -1 0.0 + "C4'" "CT" 0 -1 0.0 + "H4'" "H1" 0 -1 0.0 + "O4'" "OS" 0 -1 0.0 + "C1'" "CT" 0 -1 0.0 + "H1'" "H2" 0 -1 0.0 + "N9" "N*" 0 -1 0.0 + "C8" "CK" 0 -1 0.0 + "H8" "H5" 0 -1 0.0 + "N7" "NB" 0 -1 0.0 + "C5" "CB" 0 -1 0.0 + "C6" "CA" 0 -1 0.0 + "N6" "N2" 0 -1 0.0 + "H61" "H" 0 -1 0.0 + "H62" "H" 0 -1 0.0 + "N1" "NC" 0 -1 0.0 + "C2" "CQ" 0 -1 0.0 + "H2" "H5" 0 -1 0.0 + "N3" "NC" 0 -1 0.0 + "C4" "CB" 0 -1 0.0 + "C3'" "CT" 0 -1 0.0 + "H3'" "H1" 0 -1 0.0 + "C2'" "CT" 0 -1 0.0 + "H2'" "HC" 0 -1 0.0 + "H2''" "HC" 0 -1 0.0 + "O3'" "OS" 0 -1 0.0 +!entry.DA.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.DA.unit.childsequence single int + 2 +!entry.DA.unit.connect array int + 1 + 32 +!entry.DA.unit.connectivity table int atom1x int atom2x int flags + 1 2 1 + 1 3 1 + 1 4 1 + 4 5 1 + 5 6 1 + 5 7 1 + 5 8 1 + 8 9 1 + 8 10 1 + 8 27 1 + 10 11 1 + 11 12 1 + 11 13 1 + 11 29 1 + 13 14 1 + 13 26 1 + 14 15 1 + 14 16 1 + 16 17 1 + 17 18 1 + 17 26 1 + 18 19 1 + 18 22 1 + 19 20 1 + 19 21 1 + 22 23 1 + 23 24 1 + 23 25 1 + 25 26 1 + 27 28 1 + 27 29 1 + 27 32 1 + 29 30 1 + 29 31 1 +!entry.DA.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 + "R" 1 "A" 2 + "R" 1 "A" 3 + "R" 1 "A" 4 + "R" 1 "A" 5 + "R" 1 "A" 6 + "R" 1 "A" 7 + "R" 1 "A" 8 + "R" 1 "A" 9 + "R" 1 "A" 10 + "R" 1 "A" 11 + "R" 1 "A" 12 + "R" 1 "A" 13 + "R" 1 "A" 14 + "R" 1 "A" 15 + "R" 1 "A" 16 + "R" 1 "A" 17 + "R" 1 "A" 18 + "R" 1 "A" 19 + "R" 1 "A" 20 + "R" 1 "A" 21 + "R" 1 "A" 22 + "R" 1 "A" 23 + "R" 1 "A" 24 + "R" 1 "A" 25 + "R" 1 "A" 26 + "R" 1 "A" 27 + "R" 1 "A" 28 + "R" 1 "A" 29 + "R" 1 "A" 30 + "R" 1 "A" 31 + "R" 1 "A" 32 +!entry.DA.unit.name single str + "DA" +!entry.DA.unit.positions table dbl x dbl y dbl z + 2.31449 1.77667 -0.478436 + 1.93341 3.07337 -1.08155 + 3.15655 0.871841 -1.29245 + 2.99437 2.14248 0.922974 + 2.15604 2.55184 2.01989 + 1.45269 1.75384 2.2578 + 1.60497 3.45003 1.74118 + 3.00825 2.84792 3.24319 + 2.44712 3.47737 3.93385 + 3.22812 1.60517 3.97723 + 4.55861 1.15095 3.77755 + 5.072 1.09528 4.73746 + 4.53889 -0.297039 3.31566 + 4.33675 -0.846471 2.07705 + 4.1524 -0.207228 1.22629 + 4.45287 -2.14097 2.04908 + 4.75406 -2.48004 3.36301 + 4.998 -3.7082 3.98922 + 4.97679 -4.88349 3.34592 + 5.16072 -5.73881 3.8506 + 4.77686 -4.91403 2.35638 + 5.2653 -3.68934 5.30215 + 5.28498 -2.51987 5.93529 + 5.49828 -2.48215 6.99334 + 5.07269 -1.30851 5.45578 + 4.80614 -1.35635 4.13322 + 4.41561 3.37783 2.96131 + 4.41207 3.94149 2.02837 + 5.23044 2.0957 2.77942 + 5.66821 2.08131 1.7813 + 6.02511 2.06082 3.52467 + 5.0299 4.12865 3.99828 +!entry.DA.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 32 0 0 0 0 +!entry.DA.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "DA" 1 33 1 "n" 13 +!entry.DA.unit.residuesPdbSequenceNumber array int + 0 +!entry.DA.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.DA.unit.velocities table dbl x dbl y dbl z + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 +!entry.DC.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "P" "P" 0 1 131072 1 15 1.165900 + "OP1" "O2" 0 1 131072 2 8 -0.776100 + "OP2" "O2" 0 1 131072 3 8 -0.776100 + "O5'" "OS" 0 1 131072 4 8 -0.495400 + "C5'" "CI" 0 1 131072 5 6 -0.006900 + "H5'" "H1" 0 1 131072 6 1 0.075400 + "H5''" "H1" 0 1 131072 7 1 0.075400 + "C4'" "CT" 0 1 131072 8 6 0.162900 + "H4'" "H1" 0 1 131072 9 1 0.117600 + "O4'" "OS" 0 1 131072 10 8 -0.369100 + "C1'" "CT" 0 1 131072 11 6 -0.011600 + "H1'" "H2" 0 1 131072 12 1 0.196300 + "N1" "N*" 0 1 131072 13 7 -0.033900 + "C6" "CM" 0 1 131072 14 6 -0.018300 + "H6" "H4" 0 1 131072 15 1 0.229300 + "C5" "CM" 0 1 131072 16 6 -0.522200 + "H5" "HA" 0 1 131072 17 1 0.186300 + "C4" "CA" 0 1 131072 18 6 0.843900 + "N4" "N2" 0 1 131072 19 7 -0.977300 + "H41" "H" 0 1 131072 20 1 0.431400 + "H42" "H" 0 1 131072 21 1 0.431400 + "N3" "NC" 0 1 131072 22 7 -0.774800 + "C2" "C" 0 1 131072 23 6 0.795900 + "O2" "O" 0 1 131072 24 8 -0.654800 + "C3'" "CT" 0 1 131072 25 6 0.071300 + "H3'" "H1" 0 1 131072 26 1 0.098500 + "C2'" "CT" 0 1 131072 27 6 -0.085400 + "H2'" "HC" 0 1 131072 28 1 0.071800 + "H2''" "HC" 0 1 131072 29 1 0.071800 + "O3'" "OS" 0 1 131072 30 8 -0.523200 +!entry.DC.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "P" "P" 0 -1 0.0 + "OP1" "O2" 0 -1 0.0 + "OP2" "O2" 0 -1 0.0 + "O5'" "OS" 0 -1 0.0 + "C5'" "CI" 0 -1 0.0 + "H5'" "H1" 0 -1 0.0 + "H5''" "H1" 0 -1 0.0 + "C4'" "CT" 0 -1 0.0 + "H4'" "H1" 0 -1 0.0 + "O4'" "OS" 0 -1 0.0 + "C1'" "CT" 0 -1 0.0 + "H1'" "H2" 0 -1 0.0 + "N1" "N*" 0 -1 0.0 + "C6" "CM" 0 -1 0.0 + "H6" "H4" 0 -1 0.0 + "C5" "CM" 0 -1 0.0 + "H5" "HA" 0 -1 0.0 + "C4" "CA" 0 -1 0.0 + "N4" "N2" 0 -1 0.0 + "H41" "H" 0 -1 0.0 + "H42" "H" 0 -1 0.0 + "N3" "NC" 0 -1 0.0 + "C2" "C" 0 -1 0.0 + "O2" "O" 0 -1 0.0 + "C3'" "CT" 0 -1 0.0 + "H3'" "H1" 0 -1 0.0 + "C2'" "CT" 0 -1 0.0 + "H2'" "HC" 0 -1 0.0 + "H2''" "HC" 0 -1 0.0 + "O3'" "OS" 0 -1 0.0 +!entry.DC.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.DC.unit.childsequence single int + 2 +!entry.DC.unit.connect array int + 1 + 30 +!entry.DC.unit.connectivity table int atom1x int atom2x int flags + 1 2 1 + 1 3 1 + 1 4 1 + 4 5 1 + 5 6 1 + 5 7 1 + 5 8 1 + 8 9 1 + 8 10 1 + 8 25 1 + 10 11 1 + 11 12 1 + 11 13 1 + 11 27 1 + 13 14 1 + 13 23 1 + 14 15 1 + 14 16 1 + 16 17 1 + 16 18 1 + 18 19 1 + 18 22 1 + 19 20 1 + 19 21 1 + 22 23 1 + 23 24 1 + 25 26 1 + 25 27 1 + 25 30 1 + 27 28 1 + 27 29 1 +!entry.DC.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 + "R" 1 "A" 2 + "R" 1 "A" 3 + "R" 1 "A" 4 + "R" 1 "A" 5 + "R" 1 "A" 6 + "R" 1 "A" 7 + "R" 1 "A" 8 + "R" 1 "A" 9 + "R" 1 "A" 10 + "R" 1 "A" 11 + "R" 1 "A" 12 + "R" 1 "A" 13 + "R" 1 "A" 14 + "R" 1 "A" 15 + "R" 1 "A" 16 + "R" 1 "A" 17 + "R" 1 "A" 18 + "R" 1 "A" 19 + "R" 1 "A" 20 + "R" 1 "A" 21 + "R" 1 "A" 22 + "R" 1 "A" 23 + "R" 1 "A" 24 + "R" 1 "A" 25 + "R" 1 "A" 26 + "R" 1 "A" 27 + "R" 1 "A" 28 + "R" 1 "A" 29 + "R" 1 "A" 30 +!entry.DC.unit.name single str + "DC" +!entry.DC.unit.positions table dbl x dbl y dbl z + 2.31449 1.77667 -0.478436 + 1.93341 3.07337 -1.08155 + 3.15655 0.871841 -1.29245 + 2.99437 2.14248 0.922974 + 2.15604 2.55184 2.01989 + 1.45269 1.75384 2.2578 + 1.60497 3.45003 1.74118 + 3.00825 2.84792 3.24319 + 2.44712 3.47737 3.93385 + 3.22812 1.60517 3.97723 + 4.55861 1.15095 3.77755 + 5.072 1.09528 4.73746 + 4.50057 -0.267961 3.32653 + 4.2582 -0.582249 2.02573 + 4.10396 0.202767 1.30022 + 4.20566 -1.87808 1.61627 + 4.01025 -2.11143 0.569628 + 4.4162 -2.88083 2.6138 + 4.3789 -4.15892 2.2859 + 4.53246 -4.83639 3.01908 + 4.20177 -4.43444 1.33049 + 4.65265 -2.56446 3.8838 + 4.70124 -1.26231 4.27323 + 4.91896 -0.927584 5.44718 + 4.41561 3.37783 2.96131 + 4.41207 3.94149 2.02837 + 5.23044 2.0957 2.77942 + 5.66821 2.08131 1.7813 + 6.02511 2.06082 3.52467 + 5.0299 4.12865 3.99828 +!entry.DC.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 30 0 0 0 0 +!entry.DC.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "DC" 1 31 1 "n" 15 +!entry.DC.unit.residuesPdbSequenceNumber array int + 0 +!entry.DC.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.DC.unit.velocities table dbl x dbl y dbl z + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 +!entry.DG.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "P" "P" 0 1 131072 1 15 1.165900 + "OP1" "O2" 0 1 131072 2 8 -0.776100 + "OP2" "O2" 0 1 131072 3 8 -0.776100 + "O5'" "OS" 0 1 131072 4 8 -0.495400 + "C5'" "CI" 0 1 131072 5 6 -0.006900 + "H5'" "H1" 0 1 131072 6 1 0.075400 + "H5''" "H1" 0 1 131072 7 1 0.075400 + "C4'" "CT" 0 1 131072 8 6 0.162900 + "H4'" "H1" 0 1 131072 9 1 0.117600 + "O4'" "OS" 0 1 131072 10 8 -0.369100 + "C1'" "CT" 0 1 131072 11 6 0.035800 + "H1'" "H2" 0 1 131072 12 1 0.174600 + "N9" "N*" 0 1 131072 13 7 0.057700 + "C8" "CK" 0 1 131072 14 6 0.073600 + "H8" "H5" 0 1 131072 15 1 0.199700 + "N7" "NB" 0 1 131072 16 7 -0.572500 + "C5" "CB" 0 1 131072 17 6 0.199100 + "C6" "C" 0 1 131072 18 6 0.491800 + "O6" "O" 0 1 131072 19 8 -0.569900 + "N1" "NA" 0 1 131072 20 7 -0.505300 + "H1" "H" 0 1 131072 21 1 0.352000 + "C2" "CA" 0 1 131072 22 6 0.743200 + "N2" "N2" 0 1 131072 23 7 -0.923000 + "H21" "H" 0 1 131072 24 1 0.423500 + "H22" "H" 0 1 131072 25 1 0.423500 + "N3" "NC" 0 1 131072 26 7 -0.663600 + "C4" "CB" 0 1 131072 27 6 0.181400 + "C3'" "CT" 0 1 131072 28 6 0.071300 + "H3'" "H1" 0 1 131072 29 1 0.098500 + "C2'" "CT" 0 1 131072 30 6 -0.085400 + "H2'" "HC" 0 1 131072 31 1 0.071800 + "H2''" "HC" 0 1 131072 32 1 0.071800 + "O3'" "OS" 0 1 131072 33 8 -0.523200 +!entry.DG.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "P" "P" 0 -1 0.0 + "OP1" "O2" 0 -1 0.0 + "OP2" "O2" 0 -1 0.0 + "O5'" "OS" 0 -1 0.0 + "C5'" "CI" 0 -1 0.0 + "H5'" "H1" 0 -1 0.0 + "H5''" "H1" 0 -1 0.0 + "C4'" "CT" 0 -1 0.0 + "H4'" "H1" 0 -1 0.0 + "O4'" "OS" 0 -1 0.0 + "C1'" "CT" 0 -1 0.0 + "H1'" "H2" 0 -1 0.0 + "N9" "N*" 0 -1 0.0 + "C8" "CK" 0 -1 0.0 + "H8" "H5" 0 -1 0.0 + "N7" "NB" 0 -1 0.0 + "C5" "CB" 0 -1 0.0 + "C6" "C" 0 -1 0.0 + "O6" "O" 0 -1 0.0 + "N1" "NA" 0 -1 0.0 + "H1" "H" 0 -1 0.0 + "C2" "CA" 0 -1 0.0 + "N2" "N2" 0 -1 0.0 + "H21" "H" 0 -1 0.0 + "H22" "H" 0 -1 0.0 + "N3" "NC" 0 -1 0.0 + "C4" "CB" 0 -1 0.0 + "C3'" "CT" 0 -1 0.0 + "H3'" "H1" 0 -1 0.0 + "C2'" "CT" 0 -1 0.0 + "H2'" "HC" 0 -1 0.0 + "H2''" "HC" 0 -1 0.0 + "O3'" "OS" 0 -1 0.0 +!entry.DG.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.DG.unit.childsequence single int + 2 +!entry.DG.unit.connect array int + 1 + 33 +!entry.DG.unit.connectivity table int atom1x int atom2x int flags + 1 2 1 + 1 3 1 + 1 4 1 + 4 5 1 + 5 6 1 + 5 7 1 + 5 8 1 + 8 9 1 + 8 10 1 + 8 28 1 + 10 11 1 + 11 12 1 + 11 13 1 + 11 30 1 + 13 14 1 + 13 27 1 + 14 15 1 + 14 16 1 + 16 17 1 + 17 18 1 + 17 27 1 + 18 19 1 + 18 20 1 + 20 21 1 + 20 22 1 + 22 23 1 + 22 26 1 + 23 24 1 + 23 25 1 + 26 27 1 + 28 29 1 + 28 30 1 + 28 33 1 + 30 31 1 + 30 32 1 +!entry.DG.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 + "R" 1 "A" 2 + "R" 1 "A" 3 + "R" 1 "A" 4 + "R" 1 "A" 5 + "R" 1 "A" 6 + "R" 1 "A" 7 + "R" 1 "A" 8 + "R" 1 "A" 9 + "R" 1 "A" 10 + "R" 1 "A" 11 + "R" 1 "A" 12 + "R" 1 "A" 13 + "R" 1 "A" 14 + "R" 1 "A" 15 + "R" 1 "A" 16 + "R" 1 "A" 17 + "R" 1 "A" 18 + "R" 1 "A" 19 + "R" 1 "A" 20 + "R" 1 "A" 21 + "R" 1 "A" 22 + "R" 1 "A" 23 + "R" 1 "A" 24 + "R" 1 "A" 25 + "R" 1 "A" 26 + "R" 1 "A" 27 + "R" 1 "A" 28 + "R" 1 "A" 29 + "R" 1 "A" 30 + "R" 1 "A" 31 + "R" 1 "A" 32 + "R" 1 "A" 33 +!entry.DG.unit.name single str + "DG" +!entry.DG.unit.positions table dbl x dbl y dbl z + 2.31449 1.77667 -0.478436 + 1.93341 3.07337 -1.08155 + 3.15655 0.871841 -1.29245 + 2.99437 2.14248 0.922974 + 2.15604 2.55184 2.01989 + 1.45269 1.75384 2.2578 + 1.60497 3.45003 1.74118 + 3.00825 2.84792 3.24319 + 2.44712 3.47737 3.93385 + 3.22812 1.60517 3.97723 + 4.55861 1.15095 3.77755 + 5.072 1.09528 4.73746 + 4.49953 -0.267934 3.32658 + 4.2675 -0.772686 2.06333 + 4.09631 -0.099877 1.23603 + 4.27874 -2.0812 2.00203 + 4.5351 -2.47458 3.31033 + 4.66503 -3.77205 3.87255 + 4.57806 -4.86546 3.31596 + 4.92592 -3.7091 5.24659 + 5.03253 -4.57556 5.73431 + 5.04771 -2.54746 5.98155 + 5.29927 -2.70244 7.28857 + 5.39674 -3.57395 7.78964 + 5.38121 -1.85732 7.83553 + 4.92783 -1.33295 5.45289 + 4.674 -1.37655 4.1175 + 4.41561 3.37783 2.96131 + 4.41207 3.94149 2.02837 + 5.23044 2.0957 2.77942 + 5.66821 2.08131 1.7813 + 6.02511 2.06082 3.52467 + 5.0299 4.12865 3.99828 +!entry.DG.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 33 0 0 0 0 +!entry.DG.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "DG" 1 34 1 "n" 13 +!entry.DG.unit.residuesPdbSequenceNumber array int + 0 +!entry.DG.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.DG.unit.velocities table dbl x dbl y dbl z + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 +!entry.DT.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "P" "P" 0 1 131072 1 15 1.165900 + "OP1" "O2" 0 1 131072 2 8 -0.776100 + "OP2" "O2" 0 1 131072 3 8 -0.776100 + "O5'" "OS" 0 1 131072 4 8 -0.495400 + "C5'" "CI" 0 1 131072 5 6 -0.006900 + "H5'" "H1" 0 1 131072 6 1 0.075400 + "H5''" "H1" 0 1 131072 7 1 0.075400 + "C4'" "CT" 0 1 131072 8 6 0.162900 + "H4'" "H1" 0 1 131072 9 1 0.117600 + "O4'" "OS" 0 1 131072 10 8 -0.369100 + "C1'" "CT" 0 1 131072 11 6 0.068000 + "H1'" "H2" 0 1 131072 12 1 0.180400 + "N1" "N*" 0 1 131072 13 7 -0.023900 + "C6" "CM" 0 1 131072 14 6 -0.220900 + "H6" "H4" 0 1 131072 15 1 0.260700 + "C5" "CM" 0 1 131072 16 6 0.002500 + "C7" "CT" 0 1 131072 17 6 -0.226900 + "H71" "HC" 0 1 131072 18 1 0.077000 + "H72" "HC" 0 1 131072 19 1 0.077000 + "H73" "HC" 0 1 131072 20 1 0.077000 + "C4" "C" 0 1 131072 21 6 0.519400 + "O4" "O" 0 1 131072 22 8 -0.556300 + "N3" "NA" 0 1 131072 23 7 -0.434000 + "H3" "H" 0 1 131072 24 1 0.342000 + "C2" "C" 0 1 131072 25 6 0.567700 + "O2" "O" 0 1 131072 26 8 -0.588100 + "C3'" "CT" 0 1 131072 27 6 0.071300 + "H3'" "H1" 0 1 131072 28 1 0.098500 + "C2'" "CT" 0 1 131072 29 6 -0.085400 + "H2'" "HC" 0 1 131072 30 1 0.071800 + "H2''" "HC" 0 1 131072 31 1 0.071800 + "O3'" "OS" 0 1 131072 32 8 -0.523200 +!entry.DT.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "P" "P" 0 -1 0.0 + "OP1" "O2" 0 -1 0.0 + "OP2" "O2" 0 -1 0.0 + "O5'" "OS" 0 -1 0.0 + "C5'" "CI" 0 -1 0.0 + "H5'" "H1" 0 -1 0.0 + "H5''" "H1" 0 -1 0.0 + "C4'" "CT" 0 -1 0.0 + "H4'" "H1" 0 -1 0.0 + "O4'" "OS" 0 -1 0.0 + "C1'" "CT" 0 -1 0.0 + "H1'" "H2" 0 -1 0.0 + "N1" "N*" 0 -1 0.0 + "C6" "CM" 0 -1 0.0 + "H6" "H4" 0 -1 0.0 + "C5" "CM" 0 -1 0.0 + "C7" "CT" 0 -1 0.0 + "H71" "HC" 0 -1 0.0 + "H72" "HC" 0 -1 0.0 + "H73" "HC" 0 -1 0.0 + "C4" "C" 0 -1 0.0 + "O4" "O" 0 -1 0.0 + "N3" "NA" 0 -1 0.0 + "H3" "H" 0 -1 0.0 + "C2" "C" 0 -1 0.0 + "O2" "O" 0 -1 0.0 + "C3'" "CT" 0 -1 0.0 + "H3'" "H1" 0 -1 0.0 + "C2'" "CT" 0 -1 0.0 + "H2'" "HC" 0 -1 0.0 + "H2''" "HC" 0 -1 0.0 + "O3'" "OS" 0 -1 0.0 +!entry.DT.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.DT.unit.childsequence single int + 2 +!entry.DT.unit.connect array int + 1 + 32 +!entry.DT.unit.connectivity table int atom1x int atom2x int flags + 1 2 1 + 1 3 1 + 1 4 1 + 4 5 1 + 5 6 1 + 5 7 1 + 5 8 1 + 8 9 1 + 8 10 1 + 8 27 1 + 10 11 1 + 11 12 1 + 11 13 1 + 11 29 1 + 13 14 1 + 13 25 1 + 14 15 1 + 14 16 1 + 16 17 1 + 16 21 1 + 17 18 1 + 17 19 1 + 17 20 1 + 21 22 1 + 21 23 1 + 23 24 1 + 23 25 1 + 25 26 1 + 27 28 1 + 27 29 1 + 27 32 1 + 29 30 1 + 29 31 1 +!entry.DT.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 + "R" 1 "A" 2 + "R" 1 "A" 3 + "R" 1 "A" 4 + "R" 1 "A" 5 + "R" 1 "A" 6 + "R" 1 "A" 7 + "R" 1 "A" 8 + "R" 1 "A" 9 + "R" 1 "A" 10 + "R" 1 "A" 11 + "R" 1 "A" 12 + "R" 1 "A" 13 + "R" 1 "A" 14 + "R" 1 "A" 15 + "R" 1 "A" 16 + "R" 1 "A" 17 + "R" 1 "A" 18 + "R" 1 "A" 19 + "R" 1 "A" 20 + "R" 1 "A" 21 + "R" 1 "A" 22 + "R" 1 "A" 23 + "R" 1 "A" 24 + "R" 1 "A" 25 + "R" 1 "A" 26 + "R" 1 "A" 27 + "R" 1 "A" 28 + "R" 1 "A" 29 + "R" 1 "A" 30 + "R" 1 "A" 31 + "R" 1 "A" 32 +!entry.DT.unit.name single str + "DT" +!entry.DT.unit.positions table dbl x dbl y dbl z + 2.31449 1.77667 -0.478436 + 1.93341 3.07337 -1.08155 + 3.15655 0.871841 -1.29245 + 2.99437 2.14248 0.922974 + 2.15604 2.55184 2.01989 + 1.45269 1.75384 2.2578 + 1.60497 3.45003 1.74118 + 3.00825 2.84792 3.24319 + 2.44712 3.47737 3.93385 + 3.22812 1.60517 3.97723 + 4.55861 1.15095 3.77755 + 5.072 1.09528 4.73746 + 4.53876 -0.306565 3.31262 + 4.31691 -0.672508 2.01118 + 4.14543 0.083636 1.25935 + 4.36032 -1.95759 1.63397 + 4.11889 -2.36855 0.211713 + 4.85954 -1.89683 -0.434053 + 4.20128 -3.45213 0.127085 + 3.12015 -2.05573 -0.092873 + 4.64275 -2.99699 2.58974 + 4.70295 -4.19918 2.33674 + 4.85283 -2.53849 3.87428 + 5.07043 -3.28915 4.63406 + 4.81472 -1.22388 4.2923 + 5.01467 -0.915001 5.45549 + 4.41561 3.37783 2.96131 + 4.41207 3.94149 2.02837 + 5.23044 2.0957 2.77942 + 5.66821 2.08131 1.7813 + 6.02511 2.06082 3.52467 + 5.0299 4.12865 3.99828 +!entry.DT.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 32 0 0 0 0 +!entry.DT.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "DT" 1 33 1 "n" 15 +!entry.DT.unit.residuesPdbSequenceNumber array int + 0 +!entry.DT.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.DT.unit.velocities table dbl x dbl y dbl z + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 +!entry.F.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "F" "F-" 0 1 131072 1 9 -1.000000 +!entry.F.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "F" "F-" 0 -1 0.0 +!entry.F.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.F.unit.childsequence single int + 2 +!entry.F.unit.connect array int + 0 + 0 +!entry.F.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 +!entry.F.unit.name single str + "F" +!entry.F.unit.positions table dbl x dbl y dbl z + 0 0 0 +!entry.F.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 1 0 0 0 0 +!entry.F.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "F" 1 2 1 "?" 0 +!entry.F.unit.residuesPdbSequenceNumber array int + 0 +!entry.F.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.F.unit.velocities table dbl x dbl y dbl z + 0 0 0 +!entry.FE.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "FE" "Fe3+" 0 1 131072 1 26 3.000000 +!entry.FE.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "FE" "Fe3+" 0 -1 0.0 +!entry.FE.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.FE.unit.childsequence single int + 2 +!entry.FE.unit.connect array int + 0 + 0 +!entry.FE.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 +!entry.FE.unit.name single str + "FE" +!entry.FE.unit.positions table dbl x dbl y dbl z + 0 0 0 +!entry.FE.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 1 0 0 0 0 +!entry.FE.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "FE" 1 2 1 "?" 0 +!entry.FE.unit.residuesPdbSequenceNumber array int + 0 +!entry.FE.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.FE.unit.velocities table dbl x dbl y dbl z + 0 0 0 +!entry.G.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "P" "P" 0 1 131072 1 15 1.166200 + "OP1" "O2" 0 1 131072 2 8 -0.776000 + "OP2" "O2" 0 1 131072 3 8 -0.776000 + "O5'" "OS" 0 1 131072 4 8 -0.498900 + "C5'" "CI" 0 1 131072 5 6 0.055800 + "H5'" "H1" 0 1 131072 6 1 0.067900 + "H5''" "H1" 0 1 131072 7 1 0.067900 + "C4'" "CT" 0 1 131072 8 6 0.106500 + "H4'" "H1" 0 1 131072 9 1 0.117400 + "O4'" "OS" 0 1 131072 10 8 -0.354800 + "C1'" "CT" 0 1 131072 11 6 0.019100 + "H1'" "H2" 0 1 131072 12 1 0.200600 + "N9" "N*" 0 1 131072 13 7 0.049200 + "C8" "CP" 0 1 131072 14 6 0.137400 + "H8" "H5" 0 1 131072 15 1 0.164000 + "N7" "NB" 0 1 131072 16 7 -0.570900 + "C5" "CB" 0 1 131072 17 6 0.174400 + "C6" "C" 0 1 131072 18 6 0.477000 + "O6" "O" 0 1 131072 19 8 -0.559700 + "N1" "NA" 0 1 131072 20 7 -0.478700 + "H1" "H" 0 1 131072 21 1 0.342400 + "C2" "CA" 0 1 131072 22 6 0.765700 + "N2" "N2" 0 1 131072 23 7 -0.967200 + "H21" "H" 0 1 131072 24 1 0.436400 + "H22" "H" 0 1 131072 25 1 0.436400 + "N3" "NC" 0 1 131072 26 7 -0.632300 + "C4" "CB" 0 1 131072 27 6 0.122200 + "C3'" "CT" 0 1 131072 28 6 0.202200 + "H3'" "H1" 0 1 131072 29 1 0.061500 + "C2'" "CT" 0 1 131072 30 6 0.067000 + "H2'" "H1" 0 1 131072 31 1 0.097200 + "O2'" "OH" 0 1 131072 32 8 -0.613900 + "HO2'" "HO" 0 1 131072 33 1 0.418600 + "O3'" "OS" 0 1 131072 34 8 -0.524600 +!entry.G.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "P" "P" 0 -1 0.0 + "OP1" "O2" 0 -1 0.0 + "OP2" "O2" 0 -1 0.0 + "O5'" "OS" 0 -1 0.0 + "C5'" "CI" 0 -1 0.0 + "H5'" "H1" 0 -1 0.0 + "H5''" "H1" 0 -1 0.0 + "C4'" "CT" 0 -1 0.0 + "H4'" "H1" 0 -1 0.0 + "O4'" "OS" 0 -1 0.0 + "C1'" "CT" 0 -1 0.0 + "H1'" "H2" 0 -1 0.0 + "N9" "N*" 0 -1 0.0 + "C8" "CP" 0 -1 0.0 + "H8" "H5" 0 -1 0.0 + "N7" "NB" 0 -1 0.0 + "C5" "CB" 0 -1 0.0 + "C6" "C" 0 -1 0.0 + "O6" "O" 0 -1 0.0 + "N1" "NA" 0 -1 0.0 + "H1" "H" 0 -1 0.0 + "C2" "CA" 0 -1 0.0 + "N2" "N2" 0 -1 0.0 + "H21" "H" 0 -1 0.0 + "H22" "H" 0 -1 0.0 + "N3" "NC" 0 -1 0.0 + "C4" "CB" 0 -1 0.0 + "C3'" "CT" 0 -1 0.0 + "H3'" "H1" 0 -1 0.0 + "C2'" "CT" 0 -1 0.0 + "H2'" "H1" 0 -1 0.0 + "O2'" "OH" 0 -1 0.0 + "HO2'" "HO" 0 -1 0.0 + "O3'" "OS" 0 -1 0.0 +!entry.G.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.G.unit.childsequence single int + 2 +!entry.G.unit.connect array int + 1 + 34 +!entry.G.unit.connectivity table int atom1x int atom2x int flags + 1 2 1 + 1 3 1 + 1 4 1 + 4 5 1 + 5 6 1 + 5 7 1 + 5 8 1 + 8 9 1 + 8 10 1 + 8 28 1 + 10 11 1 + 11 12 1 + 11 13 1 + 11 30 1 + 13 14 1 + 13 27 1 + 14 15 1 + 14 16 1 + 16 17 1 + 17 18 1 + 17 27 1 + 18 19 1 + 18 20 1 + 20 21 1 + 20 22 1 + 22 23 1 + 22 26 1 + 23 24 1 + 23 25 1 + 26 27 1 + 28 29 1 + 28 30 1 + 28 34 1 + 30 31 1 + 30 32 1 + 32 33 1 +!entry.G.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 + "R" 1 "A" 2 + "R" 1 "A" 3 + "R" 1 "A" 4 + "R" 1 "A" 5 + "R" 1 "A" 6 + "R" 1 "A" 7 + "R" 1 "A" 8 + "R" 1 "A" 9 + "R" 1 "A" 10 + "R" 1 "A" 11 + "R" 1 "A" 12 + "R" 1 "A" 13 + "R" 1 "A" 14 + "R" 1 "A" 15 + "R" 1 "A" 16 + "R" 1 "A" 17 + "R" 1 "A" 18 + "R" 1 "A" 19 + "R" 1 "A" 20 + "R" 1 "A" 21 + "R" 1 "A" 22 + "R" 1 "A" 23 + "R" 1 "A" 24 + "R" 1 "A" 25 + "R" 1 "A" 26 + "R" 1 "A" 27 + "R" 1 "A" 28 + "R" 1 "A" 29 + "R" 1 "A" 30 + "R" 1 "A" 31 + "R" 1 "A" 32 + "R" 1 "A" 33 + "R" 1 "A" 34 +!entry.G.unit.name single str + "G" +!entry.G.unit.positions table dbl x dbl y dbl z + 2.31449 1.77667 -0.478436 + 1.93341 3.07337 -1.08155 + 3.15655 0.871841 -1.29245 + 2.99437 2.14248 0.922974 + 2.15604 2.55184 2.01989 + 1.45269 1.75384 2.2578 + 1.60497 3.45003 1.74118 + 3.00825 2.84792 3.24319 + 2.44712 3.47737 3.93385 + 3.22812 1.60517 3.97723 + 4.55861 1.15095 3.77755 + 5.072 1.09528 4.73746 + 4.49953 -0.267934 3.32658 + 4.2675 -0.772686 2.06333 + 4.09631 -0.099877 1.23603 + 4.27874 -2.0812 2.00203 + 4.5351 -2.47458 3.31033 + 4.66503 -3.77205 3.87255 + 4.57806 -4.86546 3.31596 + 4.92592 -3.7091 5.24659 + 5.03253 -4.57556 5.73431 + 5.04771 -2.54746 5.98155 + 5.29927 -2.70244 7.28857 + 5.39674 -3.57395 7.78964 + 5.38121 -1.85732 7.83553 + 4.92783 -1.33295 5.45289 + 4.674 -1.37655 4.1175 + 4.41561 3.37783 2.96131 + 4.41207 3.94149 2.02837 + 5.23044 2.0957 2.77942 + 5.66821 2.08131 1.7813 + 6.27298 2.04994 3.75714 + 6.75928 1.23523 3.61099 + 5.0299 4.12865 3.99828 +!entry.G.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 34 0 0 0 0 +!entry.G.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "G" 1 35 1 "n" 13 +!entry.G.unit.residuesPdbSequenceNumber array int + 0 +!entry.G.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.G.unit.velocities table dbl x dbl y dbl z + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 +!entry.GLN.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "N" "N" 0 1 131072 1 7 -0.415700 + "H" "H" 0 1 131072 2 1 0.271900 + "CA" "CX" 0 1 131072 3 6 -0.003100 + "HA" "H1" 0 1 131072 4 1 0.085000 + "CB" "2C" 0 1 131072 5 6 -0.003600 + "HB2" "HC" 0 1 131072 6 1 0.017100 + "HB3" "HC" 0 1 131072 7 1 0.017100 + "CG" "2C" 0 1 131072 8 6 -0.064500 + "HG2" "HC" 0 1 131072 9 1 0.035200 + "HG3" "HC" 0 1 131072 10 1 0.035200 + "CD" "C" 0 1 131072 11 6 0.695100 + "OE1" "O" 0 1 131072 12 8 -0.608600 + "NE2" "N" 0 1 131072 13 7 -0.940700 + "HE21" "H" 0 1 131072 14 1 0.425100 + "HE22" "H" 0 1 131072 15 1 0.425100 + "C" "C" 0 1 131072 16 6 0.597300 + "O" "O" 0 1 131072 17 8 -0.567900 +!entry.GLN.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "N" "N" 0 -1 0.0 + "H" "H" 0 -1 0.0 + "CA" "CX" 0 -1 0.0 + "HA" "H1" 0 -1 0.0 + "CB" "2C" 0 -1 0.0 + "HB2" "HC" 0 -1 0.0 + "HB3" "HC" 0 -1 0.0 + "CG" "2C" 0 -1 0.0 + "HG2" "HC" 0 -1 0.0 + "HG3" "HC" 0 -1 0.0 + "CD" "C" 0 -1 0.0 + "OE1" "O" 0 -1 0.0 + "NE2" "N" 0 -1 0.0 + "HE21" "H" 0 -1 0.0 + "HE22" "H" 0 -1 0.0 + "C" "C" 0 -1 0.0 + "O" "O" 0 -1 0.0 +!entry.GLN.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.GLN.unit.childsequence single int + 2 +!entry.GLN.unit.connect array int + 1 + 16 +!entry.GLN.unit.connectivity table int atom1x int atom2x int flags + 1 2 1 + 1 3 1 + 3 4 1 + 3 5 1 + 3 16 1 + 5 6 1 + 5 7 1 + 5 8 1 + 8 9 1 + 8 10 1 + 8 11 1 + 11 12 1 + 11 13 1 + 13 14 1 + 13 15 1 + 16 17 1 +!entry.GLN.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 + "R" 1 "A" 2 + "R" 1 "A" 3 + "R" 1 "A" 4 + "R" 1 "A" 5 + "R" 1 "A" 6 + "R" 1 "A" 7 + "R" 1 "A" 8 + "R" 1 "A" 9 + "R" 1 "A" 10 + "R" 1 "A" 11 + "R" 1 "A" 12 + "R" 1 "A" 13 + "R" 1 "A" 14 + "R" 1 "A" 15 + "R" 1 "A" 16 + "R" 1 "A" 17 +!entry.GLN.unit.name single str + "GLN" +!entry.GLN.unit.positions table dbl x dbl y dbl z + 3.32577 1.54791 -1.6072e-06 + 3.90941 0.723611 -2.73988e-06 + 3.97005 2.84579 -1.31116e-07 + 3.67166 3.40013 -0.88982 + 3.57696 3.65384 1.23214 + 2.497 3.80107 1.24138 + 3.87748 3.11579 2.1312 + 4.27419 5.0096 1.19458 + 5.35427 4.86318 1.18579 + 3.97378 5.54846 0.295972 + 3.90698 5.84844 2.4103 + 3.13896 5.40835 3.26289 + 4.45886 7.06152 2.48833 + 4.24843 7.65904 3.27497 + 5.08428 7.37621 1.76038 + 5.48554 2.70521 -4.39876e-06 + 6.00882 1.59318 -8.44977e-06 +!entry.GLN.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 16 0 0 0 0 +!entry.GLN.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "GLN" 1 18 1 "p" 5 +!entry.GLN.unit.residuesPdbSequenceNumber array int + 0 +!entry.GLN.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.GLN.unit.velocities table dbl x dbl y dbl z + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 +!entry.GLU.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "N" "N" 0 1 131072 1 7 -0.415700 + "H" "H" 0 1 131072 2 1 0.271900 + "CA" "CX" 0 1 131072 3 6 0.014500 + "HA" "H1" 0 1 131072 4 1 0.077900 + "CB" "2C" 0 1 131072 5 6 -0.007100 + "HB2" "HC" 0 1 131072 6 1 0.025600 + "HB3" "HC" 0 1 131072 7 1 0.025600 + "CG" "2C" 0 1 131072 8 6 -0.017400 + "HG2" "HC" 0 1 131072 9 1 0.043000 + "HG3" "HC" 0 1 131072 10 1 0.043000 + "CD" "C" 0 1 131072 11 6 0.680100 + "OE1" "O" 0 1 131072 12 8 -0.583800 + "OE2" "OH" 0 1 131072 13 8 -0.651100 + "HE2" "HO" 0 1 131072 14 1 0.464100 + "C" "C" 0 1 131072 15 6 0.597300 + "O" "O" 0 1 131072 16 8 -0.567900 +!entry.GLU.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "N" "N" 0 -1 0.0 + "H" "H" 0 -1 0.0 + "CA" "CX" 0 -1 0.0 + "HA" "H1" 0 -1 0.0 + "CB" "2C" 0 -1 0.0 + "HB2" "HC" 0 -1 0.0 + "HB3" "HC" 0 -1 0.0 + "CG" "2C" 0 -1 0.0 + "HG2" "HC" 0 -1 0.0 + "HG3" "HC" 0 -1 0.0 + "CD" "C" 0 -1 0.0 + "OE1" "O" 0 -1 0.0 + "OE2" "OH" 0 -1 0.0 + "HE2" "HO" 0 -1 0.0 + "C" "C" 0 -1 0.0 + "O" "O" 0 -1 0.0 +!entry.GLU.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.GLU.unit.childsequence single int + 2 +!entry.GLU.unit.connect array int + 1 + 15 +!entry.GLU.unit.connectivity table int atom1x int atom2x int flags + 1 2 1 + 1 3 1 + 3 4 1 + 3 5 1 + 3 15 1 + 5 6 1 + 5 7 1 + 5 8 1 + 8 9 1 + 8 10 1 + 8 11 1 + 11 12 1 + 11 13 1 + 13 14 1 + 15 16 1 +!entry.GLU.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 + "R" 1 "A" 2 + "R" 1 "A" 3 + "R" 1 "A" 4 + "R" 1 "A" 5 + "R" 1 "A" 6 + "R" 1 "A" 7 + "R" 1 "A" 8 + "R" 1 "A" 9 + "R" 1 "A" 10 + "R" 1 "A" 11 + "R" 1 "A" 12 + "R" 1 "A" 13 + "R" 1 "A" 14 + "R" 1 "A" 15 + "R" 1 "A" 16 +!entry.GLU.unit.name single str + "GLU" +!entry.GLU.unit.positions table dbl x dbl y dbl z + 3.32577 1.54791 -1.6072e-06 + 3.90941 0.723611 -2.73988e-06 + 3.97005 2.84579 -1.31116e-07 + 3.67166 3.40013 -0.88982 + 3.57696 3.65384 1.23214 + 2.497 3.80107 1.24138 + 3.87748 3.11579 2.1312 + 4.26733 4.99627 1.19495 + 5.34741 4.84984 1.18616 + 3.96692 5.53512 0.296342 + 3.87373 5.80537 2.42871 + 4.59459 5.67901 3.45438 + 2.85596 6.54207 2.33372 + 2.71053 6.99662 3.16668 + 5.48554 2.70521 -4.39876e-06 + 6.00882 1.59318 -8.44977e-06 +!entry.GLU.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 15 0 0 0 0 +!entry.GLU.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "GLU" 1 17 1 "p" 5 +!entry.GLU.unit.residuesPdbSequenceNumber array int + 0 +!entry.GLU.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.GLU.unit.velocities table dbl x dbl y dbl z + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 +!entry.GLY.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "N" "N" 0 1 131072 1 7 -0.415700 + "H" "H" 0 1 131072 2 1 0.271900 + "CA" "CX" 0 1 131072 3 6 -0.025200 + "HA2" "H1" 0 1 131072 4 1 0.069800 + "HA3" "H1" 0 1 131072 5 1 0.069800 + "C" "C" 0 1 131072 6 6 0.597300 + "O" "O" 0 1 131072 7 8 -0.567900 +!entry.GLY.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "N" "N" 0 -1 0.0 + "H" "H" 0 -1 0.0 + "CA" "CX" 0 -1 0.0 + "HA2" "H1" 0 -1 0.0 + "HA3" "H1" 0 -1 0.0 + "C" "C" 0 -1 0.0 + "O" "O" 0 -1 0.0 +!entry.GLY.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.GLY.unit.childsequence single int + 2 +!entry.GLY.unit.connect array int + 1 + 6 +!entry.GLY.unit.connectivity table int atom1x int atom2x int flags + 1 2 1 + 1 3 1 + 3 4 1 + 3 5 1 + 3 6 1 + 6 7 1 +!entry.GLY.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 + "R" 1 "A" 2 + "R" 1 "A" 3 + "R" 1 "A" 4 + "R" 1 "A" 5 + "R" 1 "A" 6 + "R" 1 "A" 7 +!entry.GLY.unit.name single str + "GLY" +!entry.GLY.unit.positions table dbl x dbl y dbl z + 3.32577 1.54791 -1.6072e-06 + 3.90941 0.723611 -2.73988e-06 + 3.97005 2.84579 -1.31116e-07 + 3.67166 3.40013 -0.88982 + 3.67167 3.40013 0.889824 + 5.48371 2.6867 -4.43886e-06 + 5.99337 1.56836 -8.46984e-06 +!entry.GLY.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 6 0 0 0 0 +!entry.GLY.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "GLY" 1 8 1 "p" 6 +!entry.GLY.unit.residuesPdbSequenceNumber array int + 0 +!entry.GLY.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.GLY.unit.velocities table dbl x dbl y dbl z + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 +!entry.HIS.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "N" "N" 0 1 131072 1 7 -0.347900 + "H" "H" 0 1 131072 2 1 0.274700 + "CA" "CX" 0 1 131072 3 6 -0.135400 + "HA" "H1" 0 1 131072 4 1 0.121200 + "CB" "CT" 0 1 131072 5 6 -0.041400 + "HB2" "HC" 0 1 131072 6 1 0.081000 + "HB3" "HC" 0 1 131072 7 1 0.081000 + "CG" "CC" 0 1 131072 8 6 -0.001200 + "ND1" "NA" 0 1 131072 9 7 -0.151300 + "HD1" "H" 0 1 131072 10 1 0.386600 + "CE1" "CR" 0 1 131072 11 6 -0.017000 + "HE1" "H5" 0 1 131072 12 1 0.268100 + "NE2" "NA" 0 1 131072 13 7 -0.171800 + "HE2" "H" 0 1 131072 14 1 0.391100 + "CD2" "CW" 0 1 131072 15 6 -0.114100 + "HD2" "H4" 0 1 131072 16 1 0.231700 + "C" "C" 0 1 131072 17 6 0.734100 + "O" "O" 0 1 131072 18 8 -0.589400 +!entry.HIS.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "N" "N" 0 -1 0.0 + "H" "H" 0 -1 0.0 + "CA" "CX" 0 -1 0.0 + "HA" "H1" 0 -1 0.0 + "CB" "CT" 0 -1 0.0 + "HB2" "HC" 0 -1 0.0 + "HB3" "HC" 0 -1 0.0 + "CG" "CC" 0 -1 0.0 + "ND1" "NA" 0 -1 0.0 + "HD1" "H" 0 -1 0.0 + "CE1" "CR" 0 -1 0.0 + "HE1" "H5" 0 -1 0.0 + "NE2" "NA" 0 -1 0.0 + "HE2" "H" 0 -1 0.0 + "CD2" "CW" 0 -1 0.0 + "HD2" "H4" 0 -1 0.0 + "C" "C" 0 -1 0.0 + "O" "O" 0 -1 0.0 +!entry.HIS.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.HIS.unit.childsequence single int + 2 +!entry.HIS.unit.connect array int + 1 + 17 +!entry.HIS.unit.connectivity table int atom1x int atom2x int flags + 1 2 1 + 1 3 1 + 3 4 1 + 3 5 1 + 3 17 1 + 5 6 1 + 5 7 1 + 5 8 1 + 8 9 1 + 8 15 1 + 9 10 1 + 9 11 1 + 11 12 1 + 11 13 1 + 13 14 1 + 13 15 1 + 15 16 1 + 17 18 1 +!entry.HIS.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 + "R" 1 "A" 2 + "R" 1 "A" 3 + "R" 1 "A" 4 + "R" 1 "A" 5 + "R" 1 "A" 6 + "R" 1 "A" 7 + "R" 1 "A" 8 + "R" 1 "A" 9 + "R" 1 "A" 10 + "R" 1 "A" 11 + "R" 1 "A" 12 + "R" 1 "A" 13 + "R" 1 "A" 14 + "R" 1 "A" 15 + "R" 1 "A" 16 + "R" 1 "A" 17 + "R" 1 "A" 18 +!entry.HIS.unit.name single str + "HIS" +!entry.HIS.unit.positions table dbl x dbl y dbl z + 3.32577 1.54791 -1.6072e-06 + 3.90941 0.723611 -2.73988e-06 + 3.97005 2.84579 -1.31116e-07 + 3.67166 3.40013 -0.88982 + 3.57696 3.65384 1.23214 + 2.497 3.80107 1.24138 + 3.87748 3.11579 2.1312 + 4.20081 5.02606 1.32109 + 3.94278 5.88509 2.38297 + 3.33973 5.69191 3.16981 + 4.62427 6.99764 2.1825 + 4.56305 7.81187 2.90456 + 5.29401 6.89145 1.06166 + 5.8963 7.60508 0.676854 + 5.05897 5.67887 0.492453 + 5.53774 5.41785 -0.451343 + 5.48554 2.70521 -4.39876e-06 + 6.00882 1.59318 -8.44977e-06 +!entry.HIS.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 17 0 0 0 0 +!entry.HIS.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "HIS" 1 19 1 "p" 8 +!entry.HIS.unit.residuesPdbSequenceNumber array int + 0 +!entry.HIS.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.HIS.unit.velocities table dbl x dbl y dbl z + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 +!entry.HOH.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "O" "OW" 0 1 131072 1 8 -0.820000 + "H1" "HW" 0 1 131072 2 1 0.410000 + "H2" "HW" 0 1 131072 3 1 0.410000 +!entry.HOH.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "O" "OW" 0 -1 0.0 + "H1" "HW" 0 -1 0.0 + "H2" "HW" 0 -1 0.0 +!entry.HOH.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.HOH.unit.childsequence single int + 2 +!entry.HOH.unit.connect array int + 0 + 0 +!entry.HOH.unit.connectivity table int atom1x int atom2x int flags + 1 2 1 + 1 3 1 +!entry.HOH.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 + "R" 1 "A" 2 + "R" 1 "A" 3 +!entry.HOH.unit.name single str + "HOH" +!entry.HOH.unit.positions table dbl x dbl y dbl z + 0.705023 0.743831 0.16023 + -0.070767 0.263875 0.450087 + 1.35632 0.064396 -0.014154 +!entry.HOH.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 1 0 0 0 0 +!entry.HOH.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "HOH" 1 4 1 "w" 0 +!entry.HOH.unit.residuesPdbSequenceNumber array int + 0 +!entry.HOH.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.HOH.unit.velocities table dbl x dbl y dbl z + 0 0 0 + 0 0 0 + 0 0 0 +!entry.HYP.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "N" "N" 0 1 131072 1 7 -0.254800 + "CD" "CT" 0 1 131072 2 6 0.059500 + "HD22" "H1" 0 1 131072 3 1 0.070000 + "HD23" "H1" 0 1 131072 4 1 0.070000 + "CG" "CT" 0 1 131072 5 6 0.040000 + "HG" "H1" 0 1 131072 6 1 0.041600 + "OD1" "OH" 0 1 131072 7 8 -0.613400 + "HD1" "HO" 0 1 131072 8 1 0.385100 + "CB" "CT" 0 1 131072 9 6 0.020300 + "HB2" "HC" 0 1 131072 10 1 0.042600 + "HB3" "HC" 0 1 131072 11 1 0.042600 + "CA" "CX" 0 1 131072 12 6 0.004700 + "HA" "H1" 0 1 131072 13 1 0.077000 + "C" "C" 0 1 131072 14 6 0.589600 + "O" "O" 0 1 131072 15 8 -0.574800 +!entry.HYP.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "N" "N" 0 -1 0.0 + "CD" "CT" 0 -1 0.0 + "HD22" "H1" 0 -1 0.0 + "HD23" "H1" 0 -1 0.0 + "CG" "CT" 0 -1 0.0 + "HG" "H1" 0 -1 0.0 + "OD1" "OH" 0 -1 0.0 + "HD1" "HO" 0 -1 0.0 + "CB" "CT" 0 -1 0.0 + "HB2" "HC" 0 -1 0.0 + "HB3" "HC" 0 -1 0.0 + "CA" "CX" 0 -1 0.0 + "HA" "H1" 0 -1 0.0 + "C" "C" 0 -1 0.0 + "O" "O" 0 -1 0.0 +!entry.HYP.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.HYP.unit.childsequence single int + 2 +!entry.HYP.unit.connect array int + 1 + 14 +!entry.HYP.unit.connectivity table int atom1x int atom2x int flags + 1 2 1 + 1 12 1 + 2 3 1 + 2 4 1 + 2 5 1 + 5 6 1 + 5 7 1 + 5 9 1 + 7 8 1 + 9 10 1 + 9 11 1 + 9 12 1 + 12 13 1 + 12 14 1 + 14 15 1 +!entry.HYP.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 + "R" 1 "A" 2 + "R" 1 "A" 3 + "R" 1 "A" 4 + "R" 1 "A" 5 + "R" 1 "A" 6 + "R" 1 "A" 7 + "R" 1 "A" 8 + "R" 1 "A" 9 + "R" 1 "A" 10 + "R" 1 "A" 11 + "R" 1 "A" 12 + "R" 1 "A" 13 + "R" 1 "A" 14 + "R" 1 "A" 15 +!entry.HYP.unit.name single str + "HYP" +!entry.HYP.unit.positions table dbl x dbl y dbl z + 3.32683 1.55739 -1.60394e-06 + 4.30215 0.476598 0.080119 + 4.42 0.019283 -0.902263 + 3.95589 -0.27404 0.790574 + 5.54713 1.17244 0.544693 + 6.41355 0.741636 0.042879 + 5.6879 1.00681 1.97831 + 6.2196 0.169859 2.1704 + 5.36909 2.62818 0.185227 + 5.96929 2.86186 -0.694123 + 5.69064 3.25104 1.01995 + 3.93361 2.87128 -0.104508 + 3.61147 3.48857 0.734106 + 3.50516 3.52639 -1.40978 + 2.75424 2.93906 -2.18541 +!entry.HYP.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 14 0 0 0 0 +!entry.HYP.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "HYP" 1 16 1 "p" 12 +!entry.HYP.unit.residuesPdbSequenceNumber array int + 0 +!entry.HYP.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.HYP.unit.velocities table dbl x dbl y dbl z + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 +!entry.ILE.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "N" "N" 0 1 131072 1 7 -0.415700 + "H" "H" 0 1 131072 2 1 0.271900 + "CA" "CX" 0 1 131072 3 6 -0.059700 + "HA" "H1" 0 1 131072 4 1 0.086900 + "CB" "3C" 0 1 131072 5 6 0.130300 + "HB" "HC" 0 1 131072 6 1 0.018700 + "CG2" "CT" 0 1 131072 7 6 -0.320400 + "HG21" "HC" 0 1 131072 8 1 0.088200 + "HG22" "HC" 0 1 131072 9 1 0.088200 + "HG23" "HC" 0 1 131072 10 1 0.088200 + "CG1" "2C" 0 1 131072 11 6 -0.043000 + "HG12" "HC" 0 1 131072 12 1 0.023600 + "HG13" "HC" 0 1 131072 13 1 0.023600 + "CD1" "CT" 0 1 131072 14 6 -0.066000 + "HD11" "HC" 0 1 131072 15 1 0.018600 + "HD12" "HC" 0 1 131072 16 1 0.018600 + "HD13" "HC" 0 1 131072 17 1 0.018600 + "C" "C" 0 1 131072 18 6 0.597300 + "O" "O" 0 1 131072 19 8 -0.567900 +!entry.ILE.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "N" "N" 0 -1 0.0 + "H" "H" 0 -1 0.0 + "CA" "CX" 0 -1 0.0 + "HA" "H1" 0 -1 0.0 + "CB" "3C" 0 -1 0.0 + "HB" "HC" 0 -1 0.0 + "CG2" "CT" 0 -1 0.0 + "HG21" "HC" 0 -1 0.0 + "HG22" "HC" 0 -1 0.0 + "HG23" "HC" 0 -1 0.0 + "CG1" "2C" 0 -1 0.0 + "HG12" "HC" 0 -1 0.0 + "HG13" "HC" 0 -1 0.0 + "CD1" "CT" 0 -1 0.0 + "HD11" "HC" 0 -1 0.0 + "HD12" "HC" 0 -1 0.0 + "HD13" "HC" 0 -1 0.0 + "C" "C" 0 -1 0.0 + "O" "O" 0 -1 0.0 +!entry.ILE.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.ILE.unit.childsequence single int + 2 +!entry.ILE.unit.connect array int + 1 + 18 +!entry.ILE.unit.connectivity table int atom1x int atom2x int flags + 1 2 1 + 1 3 1 + 3 4 1 + 3 5 1 + 3 18 1 + 5 6 1 + 5 7 1 + 5 11 1 + 7 8 1 + 7 9 1 + 7 10 1 + 11 12 1 + 11 13 1 + 11 14 1 + 14 15 1 + 14 16 1 + 14 17 1 + 18 19 1 +!entry.ILE.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 + "R" 1 "A" 2 + "R" 1 "A" 3 + "R" 1 "A" 4 + "R" 1 "A" 5 + "R" 1 "A" 6 + "R" 1 "A" 7 + "R" 1 "A" 8 + "R" 1 "A" 9 + "R" 1 "A" 10 + "R" 1 "A" 11 + "R" 1 "A" 12 + "R" 1 "A" 13 + "R" 1 "A" 14 + "R" 1 "A" 15 + "R" 1 "A" 16 + "R" 1 "A" 17 + "R" 1 "A" 18 + "R" 1 "A" 19 +!entry.ILE.unit.name single str + "ILE" +!entry.ILE.unit.positions table dbl x dbl y dbl z + 3.32577 1.54791 -1.6072e-06 + 3.90941 0.723611 -2.73988e-06 + 3.97005 2.84579 -1.31116e-07 + 3.67166 3.40013 -0.88982 + 3.55214 3.62073 1.24517 + 2.47013 3.75249 1.24564 + 3.97004 2.84573 2.4903 + 5.05205 2.71397 2.49076 + 3.67156 3.39921 3.38062 + 3.48565 1.86928 2.49074 + 4.2302 4.98669 1.24517 + 5.31231 4.85575 1.24516 + 3.93182 5.54103 0.355348 + 3.81229 5.76163 2.49034 + 4.11078 5.2081 3.38063 + 4.29669 6.73808 2.49083 + 2.73029 5.89338 2.49081 + 5.48554 2.70521 -4.39876e-06 + 6.00882 1.59318 -8.44977e-06 +!entry.ILE.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 18 0 0 0 0 +!entry.ILE.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "ILE" 1 20 1 "p" 5 +!entry.ILE.unit.residuesPdbSequenceNumber array int + 0 +!entry.ILE.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.ILE.unit.velocities table dbl x dbl y dbl z + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 +!entry.K.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "K" "K+" 0 1 131072 1 19 1.000000 +!entry.K.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "K" "K+" 0 -1 0.0 +!entry.K.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.K.unit.childsequence single int + 2 +!entry.K.unit.connect array int + 0 + 0 +!entry.K.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 +!entry.K.unit.name single str + "K" +!entry.K.unit.positions table dbl x dbl y dbl z + 0 0 0 +!entry.K.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 1 0 0 0 0 +!entry.K.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "K" 1 2 1 "?" 0 +!entry.K.unit.residuesPdbSequenceNumber array int + 0 +!entry.K.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.K.unit.velocities table dbl x dbl y dbl z + 0 0 0 +!entry.LEU.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "N" "N" 0 1 131072 1 7 -0.415700 + "H" "H" 0 1 131072 2 1 0.271900 + "CA" "CX" 0 1 131072 3 6 -0.051800 + "HA" "H1" 0 1 131072 4 1 0.092200 + "CB" "2C" 0 1 131072 5 6 -0.110200 + "HB2" "HC" 0 1 131072 6 1 0.045700 + "HB3" "HC" 0 1 131072 7 1 0.045700 + "CG" "3C" 0 1 131072 8 6 0.353100 + "HG" "HC" 0 1 131072 9 1 -0.036100 + "CD1" "CT" 0 1 131072 10 6 -0.412100 + "HD11" "HC" 0 1 131072 11 1 0.100000 + "HD12" "HC" 0 1 131072 12 1 0.100000 + "HD13" "HC" 0 1 131072 13 1 0.100000 + "CD2" "CT" 0 1 131072 14 6 -0.412100 + "HD21" "HC" 0 1 131072 15 1 0.100000 + "HD22" "HC" 0 1 131072 16 1 0.100000 + "HD23" "HC" 0 1 131072 17 1 0.100000 + "C" "C" 0 1 131072 18 6 0.597300 + "O" "O" 0 1 131072 19 8 -0.567900 +!entry.LEU.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "N" "N" 0 -1 0.0 + "H" "H" 0 -1 0.0 + "CA" "CX" 0 -1 0.0 + "HA" "H1" 0 -1 0.0 + "CB" "2C" 0 -1 0.0 + "HB2" "HC" 0 -1 0.0 + "HB3" "HC" 0 -1 0.0 + "CG" "3C" 0 -1 0.0 + "HG" "HC" 0 -1 0.0 + "CD1" "CT" 0 -1 0.0 + "HD11" "HC" 0 -1 0.0 + "HD12" "HC" 0 -1 0.0 + "HD13" "HC" 0 -1 0.0 + "CD2" "CT" 0 -1 0.0 + "HD21" "HC" 0 -1 0.0 + "HD22" "HC" 0 -1 0.0 + "HD23" "HC" 0 -1 0.0 + "C" "C" 0 -1 0.0 + "O" "O" 0 -1 0.0 +!entry.LEU.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.LEU.unit.childsequence single int + 2 +!entry.LEU.unit.connect array int + 1 + 18 +!entry.LEU.unit.connectivity table int atom1x int atom2x int flags + 1 2 1 + 1 3 1 + 3 4 1 + 3 5 1 + 3 18 1 + 5 6 1 + 5 7 1 + 5 8 1 + 8 9 1 + 8 10 1 + 8 14 1 + 10 11 1 + 10 12 1 + 10 13 1 + 14 15 1 + 14 16 1 + 14 17 1 + 18 19 1 +!entry.LEU.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 + "R" 1 "A" 2 + "R" 1 "A" 3 + "R" 1 "A" 4 + "R" 1 "A" 5 + "R" 1 "A" 6 + "R" 1 "A" 7 + "R" 1 "A" 8 + "R" 1 "A" 9 + "R" 1 "A" 10 + "R" 1 "A" 11 + "R" 1 "A" 12 + "R" 1 "A" 13 + "R" 1 "A" 14 + "R" 1 "A" 15 + "R" 1 "A" 16 + "R" 1 "A" 17 + "R" 1 "A" 18 + "R" 1 "A" 19 +!entry.LEU.unit.name single str + "LEU" +!entry.LEU.unit.positions table dbl x dbl y dbl z + 3.32577 1.54791 -1.6072e-06 + 3.90941 0.723611 -2.73988e-06 + 3.97005 2.84579 -1.31116e-07 + 3.67166 3.40013 -0.88982 + 3.57696 3.65384 1.23214 + 2.497 3.80107 1.24138 + 3.87748 3.11579 2.1312 + 4.27419 5.0096 1.19458 + 5.35427 4.86318 1.18579 + 3.85343 5.7629 -0.062857 + 2.77345 5.91011 -0.054557 + 4.35151 6.73205 -0.090203 + 4.13416 5.1857 -0.943846 + 3.8811 5.81764 2.42672 + 4.18163 5.2796 3.32577 + 4.3792 6.78683 2.40036 + 2.80113 5.96488 2.43596 + 5.48554 2.70521 -4.39876e-06 + 6.00882 1.59318 -8.44977e-06 +!entry.LEU.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 18 0 0 0 0 +!entry.LEU.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "LEU" 1 20 1 "p" 5 +!entry.LEU.unit.residuesPdbSequenceNumber array int + 0 +!entry.LEU.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.LEU.unit.velocities table dbl x dbl y dbl z + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 +!entry.LI.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "LI" "Li+" 0 1 131072 1 3 1.000000 +!entry.LI.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "LI" "Li+" 0 -1 0.0 +!entry.LI.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.LI.unit.childsequence single int + 2 +!entry.LI.unit.connect array int + 0 + 0 +!entry.LI.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 +!entry.LI.unit.name single str + "LI" +!entry.LI.unit.positions table dbl x dbl y dbl z + 0 0 0 +!entry.LI.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 1 0 0 0 0 +!entry.LI.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "LI" 1 2 1 "?" 0 +!entry.LI.unit.residuesPdbSequenceNumber array int + 0 +!entry.LI.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.LI.unit.velocities table dbl x dbl y dbl z + 0 0 0 +!entry.LYS.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "N" "N" 0 1 131072 1 7 -0.347900 + "H" "H" 0 1 131072 2 1 0.274700 + "CA" "CX" 0 1 131072 3 6 -0.240000 + "HA" "H1" 0 1 131072 4 1 0.142600 + "CB" "C8" 0 1 131072 5 6 -0.009400 + "HB2" "HC" 0 1 131072 6 1 0.036200 + "HB3" "HC" 0 1 131072 7 1 0.036200 + "CG" "C8" 0 1 131072 8 6 0.018700 + "HG2" "HC" 0 1 131072 9 1 0.010300 + "HG3" "HC" 0 1 131072 10 1 0.010300 + "CD" "C8" 0 1 131072 11 6 -0.047900 + "HD2" "HC" 0 1 131072 12 1 0.062100 + "HD3" "HC" 0 1 131072 13 1 0.062100 + "CE" "C8" 0 1 131072 14 6 -0.014300 + "HE2" "HP" 0 1 131072 15 1 0.113500 + "HE3" "HP" 0 1 131072 16 1 0.113500 + "NZ" "N3" 0 1 131072 17 7 -0.385400 + "HZ1" "H" 0 1 131072 18 1 0.340000 + "HZ2" "H" 0 1 131072 19 1 0.340000 + "HZ3" "H" 0 1 131072 20 1 0.340000 + "C" "C" 0 1 131072 21 6 0.734100 + "O" "O" 0 1 131072 22 8 -0.589400 +!entry.LYS.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "N" "N" 0 -1 0.0 + "H" "H" 0 -1 0.0 + "CA" "CX" 0 -1 0.0 + "HA" "H1" 0 -1 0.0 + "CB" "C8" 0 -1 0.0 + "HB2" "HC" 0 -1 0.0 + "HB3" "HC" 0 -1 0.0 + "CG" "C8" 0 -1 0.0 + "HG2" "HC" 0 -1 0.0 + "HG3" "HC" 0 -1 0.0 + "CD" "C8" 0 -1 0.0 + "HD2" "HC" 0 -1 0.0 + "HD3" "HC" 0 -1 0.0 + "CE" "C8" 0 -1 0.0 + "HE2" "HP" 0 -1 0.0 + "HE3" "HP" 0 -1 0.0 + "NZ" "N3" 0 -1 0.0 + "HZ1" "H" 0 -1 0.0 + "HZ2" "H" 0 -1 0.0 + "HZ3" "H" 0 -1 0.0 + "C" "C" 0 -1 0.0 + "O" "O" 0 -1 0.0 +!entry.LYS.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.LYS.unit.childsequence single int + 2 +!entry.LYS.unit.connect array int + 1 + 21 +!entry.LYS.unit.connectivity table int atom1x int atom2x int flags + 1 2 1 + 1 3 1 + 3 4 1 + 3 5 1 + 3 21 1 + 5 6 1 + 5 7 1 + 5 8 1 + 8 9 1 + 8 10 1 + 8 11 1 + 11 12 1 + 11 13 1 + 11 14 1 + 14 15 1 + 14 16 1 + 14 17 1 + 17 18 1 + 17 19 1 + 17 20 1 + 21 22 1 +!entry.LYS.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 + "R" 1 "A" 2 + "R" 1 "A" 3 + "R" 1 "A" 4 + "R" 1 "A" 5 + "R" 1 "A" 6 + "R" 1 "A" 7 + "R" 1 "A" 8 + "R" 1 "A" 9 + "R" 1 "A" 10 + "R" 1 "A" 11 + "R" 1 "A" 12 + "R" 1 "A" 13 + "R" 1 "A" 14 + "R" 1 "A" 15 + "R" 1 "A" 16 + "R" 1 "A" 17 + "R" 1 "A" 18 + "R" 1 "A" 19 + "R" 1 "A" 20 + "R" 1 "A" 21 + "R" 1 "A" 22 +!entry.LYS.unit.name single str + "LYS" +!entry.LYS.unit.positions table dbl x dbl y dbl z + 3.32577 1.54791 -1.6072e-06 + 3.90941 0.723611 -2.73988e-06 + 3.97005 2.84579 -1.31116e-07 + 3.67166 3.40013 -0.88982 + 3.57696 3.65384 1.23214 + 2.497 3.80107 1.24138 + 3.87748 3.11579 2.1312 + 4.27419 5.0096 1.19458 + 5.35427 4.86318 1.18579 + 3.97378 5.54846 0.295972 + 3.8811 5.81764 2.42672 + 2.80113 5.96488 2.43596 + 4.18163 5.2796 3.32577 + 4.57833 7.17341 2.38915 + 5.65841 7.02699 2.38036 + 4.27792 7.71227 1.49055 + 4.19942 7.95231 3.57686 + 4.47809 7.45337 4.40963 + 4.66119 8.85023 3.55198 + 3.19868 8.08847 3.58497 + 5.48554 2.70521 -4.39876e-06 + 6.00882 1.59318 -8.44977e-06 +!entry.LYS.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 21 0 0 0 0 +!entry.LYS.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "LYS" 1 23 1 "p" 8 +!entry.LYS.unit.residuesPdbSequenceNumber array int + 0 +!entry.LYS.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.LYS.unit.velocities table dbl x dbl y dbl z + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 +!entry.MET.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "N" "N" 0 1 131072 1 7 -0.415700 + "H" "H" 0 1 131072 2 1 0.271900 + "CA" "CX" 0 1 131072 3 6 -0.023700 + "HA" "H1" 0 1 131072 4 1 0.088000 + "CB" "2C" 0 1 131072 5 6 0.034200 + "HB2" "HC" 0 1 131072 6 1 0.024100 + "HB3" "HC" 0 1 131072 7 1 0.024100 + "CG" "2C" 0 1 131072 8 6 0.001800 + "HG2" "H1" 0 1 131072 9 1 0.044000 + "HG3" "H1" 0 1 131072 10 1 0.044000 + "SD" "S" 0 1 131072 11 16 -0.273700 + "CE" "CT" 0 1 131072 12 6 -0.053600 + "HE1" "H1" 0 1 131072 13 1 0.068400 + "HE2" "H1" 0 1 131072 14 1 0.068400 + "HE3" "H1" 0 1 131072 15 1 0.068400 + "C" "C" 0 1 131072 16 6 0.597300 + "O" "O" 0 1 131072 17 8 -0.567900 +!entry.MET.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "N" "N" 0 -1 0.0 + "H" "H" 0 -1 0.0 + "CA" "CX" 0 -1 0.0 + "HA" "H1" 0 -1 0.0 + "CB" "2C" 0 -1 0.0 + "HB2" "HC" 0 -1 0.0 + "HB3" "HC" 0 -1 0.0 + "CG" "2C" 0 -1 0.0 + "HG2" "H1" 0 -1 0.0 + "HG3" "H1" 0 -1 0.0 + "SD" "S" 0 -1 0.0 + "CE" "CT" 0 -1 0.0 + "HE1" "H1" 0 -1 0.0 + "HE2" "H1" 0 -1 0.0 + "HE3" "H1" 0 -1 0.0 + "C" "C" 0 -1 0.0 + "O" "O" 0 -1 0.0 +!entry.MET.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.MET.unit.childsequence single int + 2 +!entry.MET.unit.connect array int + 1 + 16 +!entry.MET.unit.connectivity table int atom1x int atom2x int flags + 1 2 1 + 1 3 1 + 3 4 1 + 3 5 1 + 3 16 1 + 5 6 1 + 5 7 1 + 5 8 1 + 8 9 1 + 8 10 1 + 8 11 1 + 11 12 1 + 12 13 1 + 12 14 1 + 12 15 1 + 16 17 1 +!entry.MET.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 + "R" 1 "A" 2 + "R" 1 "A" 3 + "R" 1 "A" 4 + "R" 1 "A" 5 + "R" 1 "A" 6 + "R" 1 "A" 7 + "R" 1 "A" 8 + "R" 1 "A" 9 + "R" 1 "A" 10 + "R" 1 "A" 11 + "R" 1 "A" 12 + "R" 1 "A" 13 + "R" 1 "A" 14 + "R" 1 "A" 15 + "R" 1 "A" 16 + "R" 1 "A" 17 +!entry.MET.unit.name single str + "MET" +!entry.MET.unit.positions table dbl x dbl y dbl z + 3.32577 1.54791 -1.6072e-06 + 3.90941 0.723611 -2.73988e-06 + 3.97005 2.84579 -1.31116e-07 + 3.67166 3.40013 -0.88982 + 3.57696 3.65384 1.23214 + 2.497 3.80107 1.24138 + 3.87748 3.11579 2.1312 + 4.27419 5.0096 1.19458 + 5.35427 4.86318 1.18579 + 3.97378 5.54846 0.295972 + 3.81731 5.98127 2.65171 + 4.75321 7.46313 2.34095 + 4.43358 7.90404 1.39674 + 4.58591 8.1753 3.14899 + 5.81407 7.21876 2.28655 + 5.48554 2.70521 -4.39876e-06 + 6.00882 1.59318 -8.44977e-06 +!entry.MET.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 16 0 0 0 0 +!entry.MET.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "MET" 1 18 1 "p" 8 +!entry.MET.unit.residuesPdbSequenceNumber array int + 0 +!entry.MET.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.MET.unit.velocities table dbl x dbl y dbl z + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 +!entry.MG.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "MG" "Mg2+" 0 1 131072 1 12 2.000000 +!entry.MG.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "MG" "Mg2+" 0 -1 0.0 +!entry.MG.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.MG.unit.childsequence single int + 2 +!entry.MG.unit.connect array int + 0 + 0 +!entry.MG.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 +!entry.MG.unit.name single str + "MG" +!entry.MG.unit.positions table dbl x dbl y dbl z + 0 0 0 +!entry.MG.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 1 0 0 0 0 +!entry.MG.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "MG" 1 2 1 "?" 0 +!entry.MG.unit.residuesPdbSequenceNumber array int + 0 +!entry.MG.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.MG.unit.velocities table dbl x dbl y dbl z + 0 0 0 +!entry.MN.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "MN" "Mn2+" 0 1 131072 1 25 2.000000 +!entry.MN.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "MN" "Mn2+" 0 -1 0.0 +!entry.MN.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.MN.unit.childsequence single int + 2 +!entry.MN.unit.connect array int + 0 + 0 +!entry.MN.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 +!entry.MN.unit.name single str + "MN" +!entry.MN.unit.positions table dbl x dbl y dbl z + 0 0 0 +!entry.MN.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 1 0 0 0 0 +!entry.MN.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "MN" 1 2 1 "?" 0 +!entry.MN.unit.residuesPdbSequenceNumber array int + 0 +!entry.MN.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.MN.unit.velocities table dbl x dbl y dbl z + 0 0 0 +!entry.NA.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "NA" "Na+" 0 1 131072 1 11 1.000000 +!entry.NA.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "NA" "Na+" 0 -1 0.0 +!entry.NA.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.NA.unit.childsequence single int + 2 +!entry.NA.unit.connect array int + 0 + 0 +!entry.NA.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 +!entry.NA.unit.name single str + "NA" +!entry.NA.unit.positions table dbl x dbl y dbl z + 0 0 0 +!entry.NA.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 1 0 0 0 0 +!entry.NA.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "NA" 1 2 1 "?" 0 +!entry.NA.unit.residuesPdbSequenceNumber array int + 0 +!entry.NA.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.NA.unit.velocities table dbl x dbl y dbl z + 0 0 0 +!entry.NI.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "NI" "Ni2+" 0 1 131072 1 28 2.000000 +!entry.NI.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "NI" "Ni2+" 0 -1 0.0 +!entry.NI.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.NI.unit.childsequence single int + 2 +!entry.NI.unit.connect array int + 0 + 0 +!entry.NI.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 +!entry.NI.unit.name single str + "NI" +!entry.NI.unit.positions table dbl x dbl y dbl z + 0 0 0 +!entry.NI.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 1 0 0 0 0 +!entry.NI.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "NI" 1 2 1 "?" 0 +!entry.NI.unit.residuesPdbSequenceNumber array int + 0 +!entry.NI.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.NI.unit.velocities table dbl x dbl y dbl z + 0 0 0 +!entry.NME.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "N" "N" 0 1 131072 1 7 -0.415700 + "H" "H" 0 1 131072 2 1 0.271900 + "CH3" "CT" 0 1 131072 3 6 -0.149000 + "HH31" "H1" 0 1 131072 4 1 0.097600 + "HH32" "H1" 0 1 131072 5 1 0.097600 + "HH33" "H1" 0 1 131072 6 1 0.097600 +!entry.NME.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "N" "N" 0 -1 0.0 + "H" "H" 0 -1 0.0 + "CH3" "CT" 0 -1 0.0 + "HH31" "H1" 0 -1 0.0 + "HH32" "H1" 0 -1 0.0 + "HH33" "H1" 0 -1 0.0 +!entry.NME.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.NME.unit.childsequence single int + 2 +!entry.NME.unit.connect array int + 1 + 0 +!entry.NME.unit.connectivity table int atom1x int atom2x int flags + 1 2 1 + 1 3 1 + 3 4 1 + 3 5 1 + 3 6 1 +!entry.NME.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 + "R" 1 "A" 2 + "R" 1 "A" 3 + "R" 1 "A" 4 + "R" 1 "A" 5 + "R" 1 "A" 6 +!entry.NME.unit.name single str + "NME" +!entry.NME.unit.positions table dbl x dbl y dbl z + 3.32577 1.54791 -1.6072e-06 + 3.90941 0.723611 -2.73988e-06 + 3.97005 2.84579 -1.31116e-07 + 3.2115 3.62855 2.34807e-06 + 4.59199 2.94327 0.889822 + 4.59199 2.94327 -0.889825 +!entry.NME.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 1 0 0 0 0 +!entry.NME.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "NME" 1 7 1 "p" 3 +!entry.NME.unit.residuesPdbSequenceNumber array int + 0 +!entry.NME.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.NME.unit.velocities table dbl x dbl y dbl z + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 +!entry.PHE.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "N" "N" 0 1 131072 1 7 -0.415700 + "H" "H" 0 1 131072 2 1 0.271900 + "CA" "CX" 0 1 131072 3 6 -0.002400 + "HA" "H1" 0 1 131072 4 1 0.097800 + "CB" "CT" 0 1 131072 5 6 -0.034300 + "HB2" "HC" 0 1 131072 6 1 0.029500 + "HB3" "HC" 0 1 131072 7 1 0.029500 + "CG" "CA" 0 1 131072 8 6 0.011800 + "CD1" "CA" 0 1 131072 9 6 -0.125600 + "HD1" "HA" 0 1 131072 10 1 0.133000 + "CE1" "CA" 0 1 131072 11 6 -0.170400 + "HE1" "HA" 0 1 131072 12 1 0.143000 + "CZ" "CA" 0 1 131072 13 6 -0.107200 + "HZ" "HA" 0 1 131072 14 1 0.129700 + "CE2" "CA" 0 1 131072 15 6 -0.170400 + "HE2" "HA" 0 1 131072 16 1 0.143000 + "CD2" "CA" 0 1 131072 17 6 -0.125600 + "HD2" "HA" 0 1 131072 18 1 0.133000 + "C" "C" 0 1 131072 19 6 0.597300 + "O" "O" 0 1 131072 20 8 -0.567900 +!entry.PHE.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "N" "N" 0 -1 0.0 + "H" "H" 0 -1 0.0 + "CA" "CX" 0 -1 0.0 + "HA" "H1" 0 -1 0.0 + "CB" "CT" 0 -1 0.0 + "HB2" "HC" 0 -1 0.0 + "HB3" "HC" 0 -1 0.0 + "CG" "CA" 0 -1 0.0 + "CD1" "CA" 0 -1 0.0 + "HD1" "HA" 0 -1 0.0 + "CE1" "CA" 0 -1 0.0 + "HE1" "HA" 0 -1 0.0 + "CZ" "CA" 0 -1 0.0 + "HZ" "HA" 0 -1 0.0 + "CE2" "CA" 0 -1 0.0 + "HE2" "HA" 0 -1 0.0 + "CD2" "CA" 0 -1 0.0 + "HD2" "HA" 0 -1 0.0 + "C" "C" 0 -1 0.0 + "O" "O" 0 -1 0.0 +!entry.PHE.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.PHE.unit.childsequence single int + 2 +!entry.PHE.unit.connect array int + 1 + 19 +!entry.PHE.unit.connectivity table int atom1x int atom2x int flags + 1 2 1 + 1 3 1 + 3 4 1 + 3 5 1 + 3 19 1 + 5 6 1 + 5 7 1 + 5 8 1 + 8 9 1 + 8 17 1 + 9 10 1 + 9 11 1 + 11 12 1 + 11 13 1 + 13 14 1 + 13 15 1 + 15 16 1 + 15 17 1 + 17 18 1 + 19 20 1 +!entry.PHE.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 + "R" 1 "A" 2 + "R" 1 "A" 3 + "R" 1 "A" 4 + "R" 1 "A" 5 + "R" 1 "A" 6 + "R" 1 "A" 7 + "R" 1 "A" 8 + "R" 1 "A" 9 + "R" 1 "A" 10 + "R" 1 "A" 11 + "R" 1 "A" 12 + "R" 1 "A" 13 + "R" 1 "A" 14 + "R" 1 "A" 15 + "R" 1 "A" 16 + "R" 1 "A" 17 + "R" 1 "A" 18 + "R" 1 "A" 19 + "R" 1 "A" 20 +!entry.PHE.unit.name single str + "PHE" +!entry.PHE.unit.positions table dbl x dbl y dbl z + 3.32577 1.54791 -1.6072e-06 + 3.90941 0.723611 -2.73988e-06 + 3.97005 2.84579 -1.31116e-07 + 3.67166 3.40013 -0.88982 + 3.57696 3.65384 1.23214 + 2.497 3.80107 1.24138 + 3.87748 3.11579 2.1312 + 4.20081 5.02606 1.32109 + 3.91161 5.85725 2.40989 + 3.23612 5.51384 3.1934 + 4.49001 7.12951 2.49235 + 4.26485 7.77665 3.34007 + 5.35762 7.57059 1.48602 + 5.80794 8.56114 1.55022 + 5.64682 6.73941 0.397211 + 6.32231 7.08282 -0.386295 + 5.06842 5.46714 0.314744 + 5.29358 4.82001 -0.532968 + 5.48554 2.70521 -4.39876e-06 + 6.00882 1.59318 -8.44977e-06 +!entry.PHE.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 19 0 0 0 0 +!entry.PHE.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "PHE" 1 21 1 "p" 8 +!entry.PHE.unit.residuesPdbSequenceNumber array int + 0 +!entry.PHE.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.PHE.unit.velocities table dbl x dbl y dbl z + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 +!entry.PRO.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "N" "N" 0 1 131072 1 7 -0.254800 + "CD" "CT" 0 1 131072 2 6 0.019200 + "HD2" "H1" 0 1 131072 3 1 0.039100 + "HD3" "H1" 0 1 131072 4 1 0.039100 + "CG" "CT" 0 1 131072 5 6 0.018900 + "HG2" "HC" 0 1 131072 6 1 0.021300 + "HG3" "HC" 0 1 131072 7 1 0.021300 + "CB" "CT" 0 1 131072 8 6 -0.007000 + "HB2" "HC" 0 1 131072 9 1 0.025300 + "HB3" "HC" 0 1 131072 10 1 0.025300 + "CA" "CX" 0 1 131072 11 6 -0.026600 + "HA" "H1" 0 1 131072 12 1 0.064100 + "C" "C" 0 1 131072 13 6 0.589600 + "O" "O" 0 1 131072 14 8 -0.574800 +!entry.PRO.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "N" "N" 0 -1 0.0 + "CD" "CT" 0 -1 0.0 + "HD2" "H1" 0 -1 0.0 + "HD3" "H1" 0 -1 0.0 + "CG" "CT" 0 -1 0.0 + "HG2" "HC" 0 -1 0.0 + "HG3" "HC" 0 -1 0.0 + "CB" "CT" 0 -1 0.0 + "HB2" "HC" 0 -1 0.0 + "HB3" "HC" 0 -1 0.0 + "CA" "CX" 0 -1 0.0 + "HA" "H1" 0 -1 0.0 + "C" "C" 0 -1 0.0 + "O" "O" 0 -1 0.0 +!entry.PRO.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.PRO.unit.childsequence single int + 2 +!entry.PRO.unit.connect array int + 1 + 13 +!entry.PRO.unit.connectivity table int atom1x int atom2x int flags + 1 2 1 + 1 11 1 + 2 3 1 + 2 4 1 + 2 5 1 + 5 6 1 + 5 7 1 + 5 8 1 + 8 9 1 + 8 10 1 + 8 11 1 + 11 12 1 + 11 13 1 + 13 14 1 +!entry.PRO.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 + "R" 1 "A" 2 + "R" 1 "A" 3 + "R" 1 "A" 4 + "R" 1 "A" 5 + "R" 1 "A" 6 + "R" 1 "A" 7 + "R" 1 "A" 8 + "R" 1 "A" 9 + "R" 1 "A" 10 + "R" 1 "A" 11 + "R" 1 "A" 12 + "R" 1 "A" 13 + "R" 1 "A" 14 +!entry.PRO.unit.name single str + "PRO" +!entry.PRO.unit.positions table dbl x dbl y dbl z + 3.32683 1.55739 -1.60394e-06 + 4.30215 0.476598 0.080119 + 4.42 0.019283 -0.902263 + 3.95589 -0.27404 0.790574 + 5.54713 1.17244 0.544693 + 6.41355 0.741636 0.042879 + 5.65295 1.04793 1.62238 + 5.36909 2.62818 0.185227 + 5.96929 2.86186 -0.694123 + 5.69064 3.25104 1.01995 + 3.93361 2.87128 -0.104508 + 3.61147 3.48857 0.734106 + 3.50516 3.52639 -1.40978 + 2.75424 2.93906 -2.18541 +!entry.PRO.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 13 0 0 0 0 +!entry.PRO.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "PRO" 1 15 1 "p" 11 +!entry.PRO.unit.residuesPdbSequenceNumber array int + 0 +!entry.PRO.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.PRO.unit.velocities table dbl x dbl y dbl z + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 +!entry.SER.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "N" "N" 0 1 131072 1 7 -0.415700 + "H" "H" 0 1 131072 2 1 0.271900 + "CA" "CX" 0 1 131072 3 6 -0.024900 + "HA" "H1" 0 1 131072 4 1 0.084300 + "CB" "2C" 0 1 131072 5 6 0.211700 + "HB2" "H1" 0 1 131072 6 1 0.035200 + "HB3" "H1" 0 1 131072 7 1 0.035200 + "OG" "OH" 0 1 131072 8 8 -0.654600 + "HG" "HO" 0 1 131072 9 1 0.427500 + "C" "C" 0 1 131072 10 6 0.597300 + "O" "O" 0 1 131072 11 8 -0.567900 +!entry.SER.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "N" "N" 0 -1 0.0 + "H" "H" 0 -1 0.0 + "CA" "CX" 0 -1 0.0 + "HA" "H1" 0 -1 0.0 + "CB" "2C" 0 -1 0.0 + "HB2" "H1" 0 -1 0.0 + "HB3" "H1" 0 -1 0.0 + "OG" "OH" 0 -1 0.0 + "HG" "HO" 0 -1 0.0 + "C" "C" 0 -1 0.0 + "O" "O" 0 -1 0.0 +!entry.SER.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.SER.unit.childsequence single int + 2 +!entry.SER.unit.connect array int + 1 + 10 +!entry.SER.unit.connectivity table int atom1x int atom2x int flags + 1 2 1 + 1 3 1 + 3 4 1 + 3 5 1 + 3 10 1 + 5 6 1 + 5 7 1 + 5 8 1 + 8 9 1 + 10 11 1 +!entry.SER.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 + "R" 1 "A" 2 + "R" 1 "A" 3 + "R" 1 "A" 4 + "R" 1 "A" 5 + "R" 1 "A" 6 + "R" 1 "A" 7 + "R" 1 "A" 8 + "R" 1 "A" 9 + "R" 1 "A" 10 + "R" 1 "A" 11 +!entry.SER.unit.name single str + "SER" +!entry.SER.unit.positions table dbl x dbl y dbl z + 3.32577 1.54791 -1.6072e-06 + 3.90941 0.723611 -2.73988e-06 + 3.97005 2.84579 -1.31116e-07 + 3.67166 3.40013 -0.88982 + 3.57696 3.65384 1.23214 + 2.497 3.80107 1.24138 + 3.87748 3.11579 2.1312 + 4.23075 4.92514 1.19692 + 3.98331 5.43381 1.97256 + 5.48554 2.70521 -4.39876e-06 + 6.00882 1.59318 -8.44977e-06 +!entry.SER.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 10 0 0 0 0 +!entry.SER.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "SER" 1 12 1 "p" 3 +!entry.SER.unit.residuesPdbSequenceNumber array int + 0 +!entry.SER.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.SER.unit.velocities table dbl x dbl y dbl z + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 +!entry.SOL.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "OW" "OW" 0 1 131072 1 8 -0.820000 + "HW1" "HW" 0 1 131072 2 1 0.410000 + "HW2" "HW" 0 1 131072 3 1 0.410000 +!entry.SOL.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "OW" "OW" 0 -1 0.0 + "HW1" "HW" 0 -1 0.0 + "HW2" "HW" 0 -1 0.0 +!entry.SOL.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.SOL.unit.childsequence single int + 2 +!entry.SOL.unit.connect array int + 0 + 0 +!entry.SOL.unit.connectivity table int atom1x int atom2x int flags + 1 2 1 + 1 3 1 +!entry.SOL.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 + "R" 1 "A" 2 + "R" 1 "A" 3 +!entry.SOL.unit.name single str + "SOL" +!entry.SOL.unit.positions table dbl x dbl y dbl z + 0.705023 0.743831 0.16023 + -0.070767 0.263875 0.450087 + 1.35632 0.064396 -0.014154 +!entry.SOL.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 1 0 0 0 0 +!entry.SOL.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "SOL" 1 4 1 "w" 0 +!entry.SOL.unit.residuesPdbSequenceNumber array int + 0 +!entry.SOL.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.SOL.unit.velocities table dbl x dbl y dbl z + 0 0 0 + 0 0 0 + 0 0 0 +!entry.THR.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "N" "N" 0 1 131072 1 7 -0.415700 + "H" "H" 0 1 131072 2 1 0.271900 + "CA" "CX" 0 1 131072 3 6 -0.038900 + "HA" "H1" 0 1 131072 4 1 0.100700 + "CB" "3C" 0 1 131072 5 6 0.365400 + "HB" "H1" 0 1 131072 6 1 0.004300 + "CG2" "CT" 0 1 131072 7 6 -0.243800 + "HG21" "HC" 0 1 131072 8 1 0.064200 + "HG22" "HC" 0 1 131072 9 1 0.064200 + "HG23" "HC" 0 1 131072 10 1 0.064200 + "OG1" "OH" 0 1 131072 11 8 -0.676100 + "HG1" "HO" 0 1 131072 12 1 0.410200 + "C" "C" 0 1 131072 13 6 0.597300 + "O" "O" 0 1 131072 14 8 -0.567900 +!entry.THR.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "N" "N" 0 -1 0.0 + "H" "H" 0 -1 0.0 + "CA" "CX" 0 -1 0.0 + "HA" "H1" 0 -1 0.0 + "CB" "3C" 0 -1 0.0 + "HB" "H1" 0 -1 0.0 + "CG2" "CT" 0 -1 0.0 + "HG21" "HC" 0 -1 0.0 + "HG22" "HC" 0 -1 0.0 + "HG23" "HC" 0 -1 0.0 + "OG1" "OH" 0 -1 0.0 + "HG1" "HO" 0 -1 0.0 + "C" "C" 0 -1 0.0 + "O" "O" 0 -1 0.0 +!entry.THR.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.THR.unit.childsequence single int + 2 +!entry.THR.unit.connect array int + 1 + 13 +!entry.THR.unit.connectivity table int atom1x int atom2x int flags + 1 2 1 + 1 3 1 + 3 4 1 + 3 5 1 + 3 13 1 + 5 6 1 + 5 7 1 + 5 11 1 + 7 8 1 + 7 9 1 + 7 10 1 + 11 12 1 + 13 14 1 +!entry.THR.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 + "R" 1 "A" 2 + "R" 1 "A" 3 + "R" 1 "A" 4 + "R" 1 "A" 5 + "R" 1 "A" 6 + "R" 1 "A" 7 + "R" 1 "A" 8 + "R" 1 "A" 9 + "R" 1 "A" 10 + "R" 1 "A" 11 + "R" 1 "A" 12 + "R" 1 "A" 13 + "R" 1 "A" 14 +!entry.THR.unit.name single str + "THR" +!entry.THR.unit.positions table dbl x dbl y dbl z + 3.32577 1.54791 -1.6072e-06 + 3.90941 0.723611 -2.73988e-06 + 3.97005 2.84579 -1.31116e-07 + 3.67166 3.40013 -0.88982 + 3.57696 3.65384 1.23214 + 4.07506 4.62302 1.20579 + 2.06594 3.85942 1.24438 + 1.56713 2.89063 1.27121 + 1.78443 4.43695 2.1249 + 1.7647 4.39785 0.345796 + 3.9715 2.94741 2.41121 + 3.72405 3.45608 3.18686 + 5.48554 2.70521 -4.39876e-06 + 6.00882 1.59318 -8.44977e-06 +!entry.THR.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 13 0 0 0 0 +!entry.THR.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "THR" 1 15 1 "p" 3 +!entry.THR.unit.residuesPdbSequenceNumber array int + 0 +!entry.THR.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.THR.unit.velocities table dbl x dbl y dbl z + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 +!entry.TIP3.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "O" "OW" 0 1 131072 1 8 -0.820000 + "H1" "HW" 0 1 131072 2 1 0.410000 + "H2" "HW" 0 1 131072 3 1 0.410000 +!entry.TIP3.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "O" "OW" 0 -1 0.0 + "H1" "HW" 0 -1 0.0 + "H2" "HW" 0 -1 0.0 +!entry.TIP3.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.TIP3.unit.childsequence single int + 2 +!entry.TIP3.unit.connect array int + 0 + 0 +!entry.TIP3.unit.connectivity table int atom1x int atom2x int flags + 1 2 1 + 1 3 1 +!entry.TIP3.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 + "R" 1 "A" 2 + "R" 1 "A" 3 +!entry.TIP3.unit.name single str + "TIP3" +!entry.TIP3.unit.positions table dbl x dbl y dbl z + 0.705023 0.743831 0.16023 + -0.070767 0.263875 0.450087 + 1.35632 0.064396 -0.014154 +!entry.TIP3.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 1 0 0 0 0 +!entry.TIP3.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "TIP3" 1 4 1 "w" 0 +!entry.TIP3.unit.residuesPdbSequenceNumber array int + 0 +!entry.TIP3.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.TIP3.unit.velocities table dbl x dbl y dbl z + 0 0 0 + 0 0 0 + 0 0 0 +!entry.TRP.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "N" "N" 0 1 131072 1 7 -0.415700 + "H" "H" 0 1 131072 2 1 0.271900 + "CA" "CX" 0 1 131072 3 6 -0.027500 + "HA" "H1" 0 1 131072 4 1 0.112300 + "CB" "CT" 0 1 131072 5 6 -0.005000 + "HB2" "HC" 0 1 131072 6 1 0.033900 + "HB3" "HC" 0 1 131072 7 1 0.033900 + "CG" "C*" 0 1 131072 8 6 -0.141500 + "CD1" "CW" 0 1 131072 9 6 -0.163800 + "HD1" "H4" 0 1 131072 10 1 0.206200 + "NE1" "NA" 0 1 131072 11 7 -0.341800 + "HE1" "H" 0 1 131072 12 1 0.341200 + "CE2" "CN" 0 1 131072 13 6 0.138000 + "CZ2" "CA" 0 1 131072 14 6 -0.260100 + "HZ2" "HA" 0 1 131072 15 1 0.157200 + "CH2" "CA" 0 1 131072 16 6 -0.113400 + "HH2" "HA" 0 1 131072 17 1 0.141700 + "CZ3" "CA" 0 1 131072 18 6 -0.197200 + "HZ3" "HA" 0 1 131072 19 1 0.144700 + "CE3" "CA" 0 1 131072 20 6 -0.238700 + "HE3" "HA" 0 1 131072 21 1 0.170000 + "CD2" "CB" 0 1 131072 22 6 0.124300 + "C" "C" 0 1 131072 23 6 0.597300 + "O" "O" 0 1 131072 24 8 -0.567900 +!entry.TRP.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "N" "N" 0 -1 0.0 + "H" "H" 0 -1 0.0 + "CA" "CX" 0 -1 0.0 + "HA" "H1" 0 -1 0.0 + "CB" "CT" 0 -1 0.0 + "HB2" "HC" 0 -1 0.0 + "HB3" "HC" 0 -1 0.0 + "CG" "C*" 0 -1 0.0 + "CD1" "CW" 0 -1 0.0 + "HD1" "H4" 0 -1 0.0 + "NE1" "NA" 0 -1 0.0 + "HE1" "H" 0 -1 0.0 + "CE2" "CN" 0 -1 0.0 + "CZ2" "CA" 0 -1 0.0 + "HZ2" "HA" 0 -1 0.0 + "CH2" "CA" 0 -1 0.0 + "HH2" "HA" 0 -1 0.0 + "CZ3" "CA" 0 -1 0.0 + "HZ3" "HA" 0 -1 0.0 + "CE3" "CA" 0 -1 0.0 + "HE3" "HA" 0 -1 0.0 + "CD2" "CB" 0 -1 0.0 + "C" "C" 0 -1 0.0 + "O" "O" 0 -1 0.0 +!entry.TRP.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.TRP.unit.childsequence single int + 2 +!entry.TRP.unit.connect array int + 1 + 23 +!entry.TRP.unit.connectivity table int atom1x int atom2x int flags + 1 2 1 + 1 3 1 + 3 4 1 + 3 5 1 + 3 23 1 + 5 6 1 + 5 7 1 + 5 8 1 + 8 9 1 + 8 22 1 + 9 10 1 + 9 11 1 + 11 12 1 + 11 13 1 + 13 14 1 + 13 22 1 + 14 15 1 + 14 16 1 + 16 17 1 + 16 18 1 + 18 19 1 + 18 20 1 + 20 21 1 + 20 22 1 + 23 24 1 +!entry.TRP.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 + "R" 1 "A" 2 + "R" 1 "A" 3 + "R" 1 "A" 4 + "R" 1 "A" 5 + "R" 1 "A" 6 + "R" 1 "A" 7 + "R" 1 "A" 8 + "R" 1 "A" 9 + "R" 1 "A" 10 + "R" 1 "A" 11 + "R" 1 "A" 12 + "R" 1 "A" 13 + "R" 1 "A" 14 + "R" 1 "A" 15 + "R" 1 "A" 16 + "R" 1 "A" 17 + "R" 1 "A" 18 + "R" 1 "A" 19 + "R" 1 "A" 20 + "R" 1 "A" 21 + "R" 1 "A" 22 + "R" 1 "A" 23 + "R" 1 "A" 24 +!entry.TRP.unit.name single str + "TRP" +!entry.TRP.unit.positions table dbl x dbl y dbl z + 3.32577 1.54791 -1.6072e-06 + 3.90941 0.723611 -2.73988e-06 + 3.97005 2.84579 -1.31116e-07 + 3.67166 3.40013 -0.88982 + 3.57696 3.65384 1.23214 + 2.497 3.80107 1.24138 + 3.87748 3.11579 2.1312 + 4.20081 5.02606 1.32109 + 4.02345 5.93108 2.29324 + 3.36884 5.70547 3.13507 + 4.81194 7.07355 1.94981 + 4.88292 7.92201 2.49312 + 5.42735 6.84206 0.816764 + 6.29716 7.68905 0.119605 + 6.53123 8.67665 0.51705 + 6.81409 7.18701 -1.06902 + 7.49807 7.79186 -1.66436 + 6.48266 5.95312 -1.5051 + 6.89766 5.57565 -2.43965 + 5.60404 5.11735 -0.785636 + 5.35872 4.12657 -1.16808 + 5.08339 5.623 0.411545 + 5.48554 2.70521 -4.39876e-06 + 6.00882 1.59318 -8.44977e-06 +!entry.TRP.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 23 0 0 0 0 +!entry.TRP.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "TRP" 1 25 1 "p" 22 +!entry.TRP.unit.residuesPdbSequenceNumber array int + 0 +!entry.TRP.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.TRP.unit.velocities table dbl x dbl y dbl z + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 +!entry.TYR.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "N" "N" 0 1 131072 1 7 -0.415700 + "H" "H" 0 1 131072 2 1 0.271900 + "CA" "CX" 0 1 131072 3 6 -0.001400 + "HA" "H1" 0 1 131072 4 1 0.087600 + "CB" "CT" 0 1 131072 5 6 -0.015200 + "HB2" "HC" 0 1 131072 6 1 0.029500 + "HB3" "HC" 0 1 131072 7 1 0.029500 + "CG" "CA" 0 1 131072 8 6 -0.001100 + "CD1" "CA" 0 1 131072 9 6 -0.190600 + "HD1" "HA" 0 1 131072 10 1 0.169900 + "CE1" "CA" 0 1 131072 11 6 -0.234100 + "HE1" "HA" 0 1 131072 12 1 0.165600 + "CZ" "C" 0 1 131072 13 6 0.322600 + "OH" "OH" 0 1 131072 14 8 -0.557900 + "HH" "HO" 0 1 131072 15 1 0.399200 + "CE2" "CA" 0 1 131072 16 6 -0.234100 + "HE2" "HA" 0 1 131072 17 1 0.165600 + "CD2" "CA" 0 1 131072 18 6 -0.190600 + "HD2" "HA" 0 1 131072 19 1 0.169900 + "C" "C" 0 1 131072 20 6 0.597300 + "O" "O" 0 1 131072 21 8 -0.567900 +!entry.TYR.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "N" "N" 0 -1 0.0 + "H" "H" 0 -1 0.0 + "CA" "CX" 0 -1 0.0 + "HA" "H1" 0 -1 0.0 + "CB" "CT" 0 -1 0.0 + "HB2" "HC" 0 -1 0.0 + "HB3" "HC" 0 -1 0.0 + "CG" "CA" 0 -1 0.0 + "CD1" "CA" 0 -1 0.0 + "HD1" "HA" 0 -1 0.0 + "CE1" "CA" 0 -1 0.0 + "HE1" "HA" 0 -1 0.0 + "CZ" "C" 0 -1 0.0 + "OH" "OH" 0 -1 0.0 + "HH" "HO" 0 -1 0.0 + "CE2" "CA" 0 -1 0.0 + "HE2" "HA" 0 -1 0.0 + "CD2" "CA" 0 -1 0.0 + "HD2" "HA" 0 -1 0.0 + "C" "C" 0 -1 0.0 + "O" "O" 0 -1 0.0 +!entry.TYR.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.TYR.unit.childsequence single int + 2 +!entry.TYR.unit.connect array int + 1 + 20 +!entry.TYR.unit.connectivity table int atom1x int atom2x int flags + 1 2 1 + 1 3 1 + 3 4 1 + 3 5 1 + 3 20 1 + 5 6 1 + 5 7 1 + 5 8 1 + 8 9 1 + 8 18 1 + 9 10 1 + 9 11 1 + 11 12 1 + 11 13 1 + 13 14 1 + 13 16 1 + 14 15 1 + 16 17 1 + 16 18 1 + 18 19 1 + 20 21 1 +!entry.TYR.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 + "R" 1 "A" 2 + "R" 1 "A" 3 + "R" 1 "A" 4 + "R" 1 "A" 5 + "R" 1 "A" 6 + "R" 1 "A" 7 + "R" 1 "A" 8 + "R" 1 "A" 9 + "R" 1 "A" 10 + "R" 1 "A" 11 + "R" 1 "A" 12 + "R" 1 "A" 13 + "R" 1 "A" 14 + "R" 1 "A" 15 + "R" 1 "A" 16 + "R" 1 "A" 17 + "R" 1 "A" 18 + "R" 1 "A" 19 + "R" 1 "A" 20 + "R" 1 "A" 21 +!entry.TYR.unit.name single str + "TYR" +!entry.TYR.unit.positions table dbl x dbl y dbl z + 3.32577 1.54791 -1.6072e-06 + 3.90941 0.723611 -2.73988e-06 + 3.97005 2.84579 -1.31116e-07 + 3.67166 3.40013 -0.88982 + 3.57696 3.65384 1.23214 + 2.497 3.80107 1.24138 + 3.87748 3.11579 2.1312 + 4.26733 4.99627 1.19495 + 4.05993 5.91891 2.22728 + 3.40011 5.66822 3.05788 + 4.7 7.16355 2.19279 + 4.53852 7.88189 2.99654 + 5.54747 7.48554 1.12597 + 6.16925 8.69462 1.09247 + 5.95633 9.24698 1.84821 + 5.75488 6.5629 0.093635 + 6.41469 6.8136 -0.736962 + 5.11481 5.31826 0.128119 + 5.27629 4.59992 -0.675627 + 5.48554 2.70521 -4.39876e-06 + 6.00882 1.59318 -8.44977e-06 +!entry.TYR.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 20 0 0 0 0 +!entry.TYR.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "TYR" 1 22 1 "p" 8 +!entry.TYR.unit.residuesPdbSequenceNumber array int + 0 +!entry.TYR.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.TYR.unit.velocities table dbl x dbl y dbl z + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 +!entry.U.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "P" "P" 0 1 131072 1 15 1.166200 + "OP1" "O2" 0 1 131072 2 8 -0.776000 + "OP2" "O2" 0 1 131072 3 8 -0.776000 + "O5'" "OS" 0 1 131072 4 8 -0.498900 + "C5'" "CI" 0 1 131072 5 6 0.055800 + "H5'" "H1" 0 1 131072 6 1 0.067900 + "H5''" "H1" 0 1 131072 7 1 0.067900 + "C4'" "CT" 0 1 131072 8 6 0.106500 + "H4'" "H1" 0 1 131072 9 1 0.117400 + "O4'" "OS" 0 1 131072 10 8 -0.354800 + "C1'" "CT" 0 1 131072 11 6 0.067400 + "H1'" "H2" 0 1 131072 12 1 0.182400 + "N1" "N*" 0 1 131072 13 7 0.041800 + "C6" "CS" 0 1 131072 14 6 -0.112600 + "H6" "H4" 0 1 131072 15 1 0.218800 + "C5" "CS" 0 1 131072 16 6 -0.363500 + "H5" "HA" 0 1 131072 17 1 0.181100 + "C4" "C" 0 1 131072 18 6 0.595200 + "O4" "O" 0 1 131072 19 8 -0.576100 + "N3" "NA" 0 1 131072 20 7 -0.354900 + "H3" "H" 0 1 131072 21 1 0.315400 + "C2" "C" 0 1 131072 22 6 0.468700 + "O2" "O" 0 1 131072 23 8 -0.547700 + "C3'" "CT" 0 1 131072 24 6 0.202200 + "H3'" "H1" 0 1 131072 25 1 0.061500 + "C2'" "CT" 0 1 131072 26 6 0.067000 + "H2'" "H1" 0 1 131072 27 1 0.097200 + "O2'" "OH" 0 1 131072 28 8 -0.613900 + "HO2'" "HO" 0 1 131072 29 1 0.418600 + "O3'" "OS" 0 1 131072 30 8 -0.524600 +!entry.U.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "P" "P" 0 -1 0.0 + "OP1" "O2" 0 -1 0.0 + "OP2" "O2" 0 -1 0.0 + "O5'" "OS" 0 -1 0.0 + "C5'" "CI" 0 -1 0.0 + "H5'" "H1" 0 -1 0.0 + "H5''" "H1" 0 -1 0.0 + "C4'" "CT" 0 -1 0.0 + "H4'" "H1" 0 -1 0.0 + "O4'" "OS" 0 -1 0.0 + "C1'" "CT" 0 -1 0.0 + "H1'" "H2" 0 -1 0.0 + "N1" "N*" 0 -1 0.0 + "C6" "CS" 0 -1 0.0 + "H6" "H4" 0 -1 0.0 + "C5" "CS" 0 -1 0.0 + "H5" "HA" 0 -1 0.0 + "C4" "C" 0 -1 0.0 + "O4" "O" 0 -1 0.0 + "N3" "NA" 0 -1 0.0 + "H3" "H" 0 -1 0.0 + "C2" "C" 0 -1 0.0 + "O2" "O" 0 -1 0.0 + "C3'" "CT" 0 -1 0.0 + "H3'" "H1" 0 -1 0.0 + "C2'" "CT" 0 -1 0.0 + "H2'" "H1" 0 -1 0.0 + "O2'" "OH" 0 -1 0.0 + "HO2'" "HO" 0 -1 0.0 + "O3'" "OS" 0 -1 0.0 +!entry.U.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.U.unit.childsequence single int + 2 +!entry.U.unit.connect array int + 1 + 30 +!entry.U.unit.connectivity table int atom1x int atom2x int flags + 1 2 1 + 1 3 1 + 1 4 1 + 4 5 1 + 5 6 1 + 5 7 1 + 5 8 1 + 8 9 1 + 8 10 1 + 8 24 1 + 10 11 1 + 11 12 1 + 11 13 1 + 11 26 1 + 13 14 1 + 13 22 1 + 14 15 1 + 14 16 1 + 16 17 1 + 16 18 1 + 18 19 1 + 18 20 1 + 20 21 1 + 20 22 1 + 22 23 1 + 24 25 1 + 24 26 1 + 24 30 1 + 26 27 1 + 26 28 1 + 28 29 1 +!entry.U.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 + "R" 1 "A" 2 + "R" 1 "A" 3 + "R" 1 "A" 4 + "R" 1 "A" 5 + "R" 1 "A" 6 + "R" 1 "A" 7 + "R" 1 "A" 8 + "R" 1 "A" 9 + "R" 1 "A" 10 + "R" 1 "A" 11 + "R" 1 "A" 12 + "R" 1 "A" 13 + "R" 1 "A" 14 + "R" 1 "A" 15 + "R" 1 "A" 16 + "R" 1 "A" 17 + "R" 1 "A" 18 + "R" 1 "A" 19 + "R" 1 "A" 20 + "R" 1 "A" 21 + "R" 1 "A" 22 + "R" 1 "A" 23 + "R" 1 "A" 24 + "R" 1 "A" 25 + "R" 1 "A" 26 + "R" 1 "A" 27 + "R" 1 "A" 28 + "R" 1 "A" 29 + "R" 1 "A" 30 +!entry.U.unit.name single str + "U" +!entry.U.unit.positions table dbl x dbl y dbl z + 2.31449 1.77667 -0.478436 + 1.93341 3.07337 -1.08155 + 3.15655 0.871841 -1.29245 + 2.99437 2.14248 0.922974 + 2.15604 2.55184 2.01989 + 1.45269 1.75384 2.2578 + 1.60497 3.45003 1.74118 + 3.00825 2.84792 3.24319 + 2.44712 3.47737 3.93385 + 3.22812 1.60517 3.97723 + 4.55861 1.15095 3.77755 + 5.072 1.09528 4.73746 + 4.53876 -0.306565 3.31262 + 4.31691 -0.672508 2.01118 + 4.14543 0.083636 1.25935 + 4.36032 -1.95759 1.63397 + 4.1807 -2.22641 0.593026 + 4.64275 -2.99699 2.58974 + 4.70295 -4.19918 2.33674 + 4.85283 -2.53849 3.87428 + 5.07043 -3.28915 4.63406 + 4.81472 -1.22388 4.2923 + 5.01467 -0.915001 5.45549 + 4.41561 3.37783 2.96131 + 4.41207 3.94149 2.02837 + 5.23044 2.0957 2.77942 + 5.66821 2.08131 1.7813 + 6.27298 2.04994 3.75714 + 6.75928 1.23523 3.61099 + 5.0299 4.12865 3.99828 +!entry.U.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 30 0 0 0 0 +!entry.U.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "U" 1 31 1 "n" 15 +!entry.U.unit.residuesPdbSequenceNumber array int + 0 +!entry.U.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.U.unit.velocities table dbl x dbl y dbl z + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 +!entry.VAL.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "N" "N" 0 1 131072 1 7 -0.415700 + "H" "H" 0 1 131072 2 1 0.271900 + "CA" "CX" 0 1 131072 3 6 -0.087500 + "HA" "H1" 0 1 131072 4 1 0.096900 + "CB" "3C" 0 1 131072 5 6 0.298500 + "HB" "HC" 0 1 131072 6 1 -0.029700 + "CG1" "CT" 0 1 131072 7 6 -0.319200 + "HG11" "HC" 0 1 131072 8 1 0.079100 + "HG12" "HC" 0 1 131072 9 1 0.079100 + "HG13" "HC" 0 1 131072 10 1 0.079100 + "CG2" "CT" 0 1 131072 11 6 -0.319200 + "HG21" "HC" 0 1 131072 12 1 0.079100 + "HG22" "HC" 0 1 131072 13 1 0.079100 + "HG23" "HC" 0 1 131072 14 1 0.079100 + "C" "C" 0 1 131072 15 6 0.597300 + "O" "O" 0 1 131072 16 8 -0.567900 +!entry.VAL.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "N" "N" 0 -1 0.0 + "H" "H" 0 -1 0.0 + "CA" "CX" 0 -1 0.0 + "HA" "H1" 0 -1 0.0 + "CB" "3C" 0 -1 0.0 + "HB" "HC" 0 -1 0.0 + "CG1" "CT" 0 -1 0.0 + "HG11" "HC" 0 -1 0.0 + "HG12" "HC" 0 -1 0.0 + "HG13" "HC" 0 -1 0.0 + "CG2" "CT" 0 -1 0.0 + "HG21" "HC" 0 -1 0.0 + "HG22" "HC" 0 -1 0.0 + "HG23" "HC" 0 -1 0.0 + "C" "C" 0 -1 0.0 + "O" "O" 0 -1 0.0 +!entry.VAL.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.VAL.unit.childsequence single int + 2 +!entry.VAL.unit.connect array int + 1 + 15 +!entry.VAL.unit.connectivity table int atom1x int atom2x int flags + 1 2 1 + 1 3 1 + 3 4 1 + 3 5 1 + 3 15 1 + 5 6 1 + 5 7 1 + 5 11 1 + 7 8 1 + 7 9 1 + 7 10 1 + 11 12 1 + 11 13 1 + 11 14 1 + 15 16 1 +!entry.VAL.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 + "R" 1 "A" 2 + "R" 1 "A" 3 + "R" 1 "A" 4 + "R" 1 "A" 5 + "R" 1 "A" 6 + "R" 1 "A" 7 + "R" 1 "A" 8 + "R" 1 "A" 9 + "R" 1 "A" 10 + "R" 1 "A" 11 + "R" 1 "A" 12 + "R" 1 "A" 13 + "R" 1 "A" 14 + "R" 1 "A" 15 + "R" 1 "A" 16 +!entry.VAL.unit.name single str + "VAL" +!entry.VAL.unit.positions table dbl x dbl y dbl z + 3.32577 1.54791 -1.6072e-06 + 3.90941 0.723611 -2.73988e-06 + 3.97005 2.84579 -1.31116e-07 + 3.67166 3.40013 -0.88982 + 3.57696 3.65384 1.23214 + 2.497 3.80107 1.24138 + 3.99771 2.90048 2.48954 + 5.07769 2.75326 2.48124 + 3.71697 3.47763 3.37056 + 3.49963 1.93132 2.51683 + 4.27419 5.0096 1.19458 + 3.97378 5.54846 0.295972 + 3.99356 5.58758 2.07508 + 5.35427 4.86318 1.18579 + 5.48554 2.70521 -4.39876e-06 + 6.00882 1.59318 -8.44977e-06 +!entry.VAL.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 15 0 0 0 0 +!entry.VAL.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "VAL" 1 17 1 "p" 3 +!entry.VAL.unit.residuesPdbSequenceNumber array int + 0 +!entry.VAL.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.VAL.unit.velocities table dbl x dbl y dbl z + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 + 0 0 0 +!entry.WAT.unit.atoms table str name str type int typex int resx int flags int seq int elmnt dbl chg + "O" "OW" 0 1 131072 1 8 -0.820000 + "H1" "HW" 0 1 131072 2 1 0.410000 + "H2" "HW" 0 1 131072 3 1 0.410000 +!entry.WAT.unit.atomspertinfo table str pname str ptype int ptypex int pelmnt dbl pchg + "O" "OW" 0 -1 0.0 + "H1" "HW" 0 -1 0.0 + "H2" "HW" 0 -1 0.0 +!entry.WAT.unit.boundbox array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.WAT.unit.childsequence single int + 2 +!entry.WAT.unit.connect array int + 0 + 0 +!entry.WAT.unit.connectivity table int atom1x int atom2x int flags + 1 2 1 + 1 3 1 +!entry.WAT.unit.hierarchy table str abovetype int abovex str belowtype int belowx + "U" 0 "R" 1 + "R" 1 "A" 1 + "R" 1 "A" 2 + "R" 1 "A" 3 +!entry.WAT.unit.name single str + "WAT" +!entry.WAT.unit.positions table dbl x dbl y dbl z + 2.57322 -1.03407 -1.7207 + 2.49262 -1.94861 -1.99153 + 2.15966 -0.537364 -2.42673 +!entry.WAT.unit.residueconnect table int c1x int c2x int c3x int c4x int c5x int c6x + 1 1 0 0 0 0 +!entry.WAT.unit.residues table str name int seq int childseq int startatomx str restype int imagingx + "WAT" 1 4 1 "w" 0 +!entry.WAT.unit.residuesPdbSequenceNumber array int + 0 +!entry.WAT.unit.solventcap array dbl + -1.000000 + 0.0 + 0.0 + 0.0 + 0.0 +!entry.WAT.unit.velocities table dbl x dbl y dbl z + 0 0 0 + 0 0 0 + 0 0 0 diff --git a/pmx/workflow/parmed/modeller/residue.py b/pmx/workflow/parmed/modeller/residue.py new file mode 100644 index 00000000..9cf49ae4 --- /dev/null +++ b/pmx/workflow/parmed/modeller/residue.py @@ -0,0 +1,999 @@ +""" +This contains the basic residue template and residue building libraries +typically used in modelling applications +""" +from collections import OrderedDict, defaultdict +from collections.abc import Sequence +import copy as _copy +import numpy as np +import os +from ..residue import AminoAcidResidue, RNAResidue, DNAResidue +from ..topologyobjects import Atom, Bond, AtomList, TrackedList +from ..exceptions import IncompatiblePatchError, MoleculeError +import warnings + +__all__ = [ + 'PROTEIN', 'NUCLEIC', 'SOLVENT', 'UNKNOWN', 'ResidueTemplate', 'PatchTemplate', + 'ResidueTemplateContainer' +] + +class _ResidueType: + """ Singleton for various types of residues """ + def __init__(self, name): + self.name = name + + def __repr__(self): + return f'' + + def __str__(self): + return self.name + +PROTEIN = _ResidueType('PROTEIN') +NUCLEIC = _ResidueType('NUCLEIC') +SOLVENT = _ResidueType('SOLVENT') +UNKNOWN = _ResidueType('UNKNOWN') + +class ResidueTemplate(object): + """ + This is a residue template, which contains a listing of the atoms in the + residue template as well as a mapping of which atoms are bonded to other + atoms. + + Parameters + ---------- + name : str, optional + If provided, this is the name of the residue + + Attributes + ---------- + atoms : :class:`AtomList` + List of atoms in this residue + bonds : :class:`TrackedList` + List of the bonds between the atoms in this residue + coordinates : np.ndarray(natom, 3) + The partial atomic coordinates + connections : list of :class:`Atom` + A list of all atoms that should form connections with atoms of another + residue *besides* the head and tail atoms + head : :class:`Atom` or None + The atom that is connected to the residue that comes before this one + tail : :class:`Atom` or None + The atom that is connected to the *next* residue after this one + first_patch : :class:`ResidueTemplate` or None + If it is not None, this is the patch whose tail is added to the head + atom of this residue when this residue is the first in a chain + last_patch : :class:`ResidueTemplate` or None + If it is not None, this is the patch whose head is added to the tail + atom of this residue when this residue is the last in a chain + groups : list of list(:class:`Atom`) + If set, each group is a list of Atom instances making up each group + override_level : integer + For use with OpenMM ResidueTemplates. If OpenMM ForceField is given multiple + identically-matching residue templates with the same names it choses + (overrides with) the one with the highest override_level + (overrideLevel in OpenMM). Default is 0. + """ + + def __init__(self, name=''): + self.atoms = AtomList() + self.bonds = TrackedList() + self.lonepairs = list() # TODO: Should this be a TrackedList? + self.anisotropies = list() + self.name = name + self.head = None + self.tail = None + self.connections = [] + self.type = UNKNOWN + self.first_patch = None + self.last_patch = None + self.groups = [] + self.override_level = 0 + self._map = dict() + self._impr = [] + + def __repr__(self): + if self.head is not None: + head = self.head.name + else: + head = 'None' + if self.tail is not None: + tail = self.tail.name + else: + tail = 'None' + return f'<{self.__class__.__name__} {self.name}: {len(self.atoms)} atoms; {len(self.bonds)} bonds; head={head}; tail={tail}>' + + @property + def map(self): + return self._map + + def add_atom(self, atom): + """ Adds an atom to this residue template + + Parameters + ---------- + atom : :class:`Atom` + The atom to add to this residue + + Raises + ------ + ValueError if ``atom`` has the same name as another atom in this + residue already + """ + if atom.name in self._map: + raise ValueError(f'Residue already has atom named {atom.name}') + atom.residue = self + self.atoms.append(atom) + self._map[atom.name] = atom + + def delete_atom(self, atom): + """ Delete an atom from this residue template, along with corresponding bonds. + + Parameters + ---------- + atom : :class:`Atom` or str + The atom or atom name to be deleted + + """ + if type(atom) is str: + atom_name = atom + else: + atom_name = atom.name + + if atom_name not in self._map: + raise KeyError( + f"Could not find atom '{atom_name}' in ResidueTemplate, which contains atoms: {list(self._map.keys())}" + ) + + atom = self._map[atom_name] + + # Adjust head and tail if needed + if self.head == atom: + self.head = None + if self.tail == atom: + self.tail = None + + # Remove all bonds involving this atom + for bond in list(self.bonds): + if (bond.atom1 == atom) or (bond.atom2 == atom): + self.delete_bond(bond) + + # Remove all impropers involving this atom + for impr in list(self._impr): + if atom in impr: + self._impr.remove(impr) + + # Disconnect the atom from the residue so that it does not trigger atom.residue.delete_atom(atom) + atom.residue = None + + # Remove the atom from the ResidueTemplate + del self._map[atom_name] + self.atoms.remove(atom) + + def add_bond(self, atom1, atom2, order=1.0): + """ Adds a bond between the two provided atoms in the residue + + Parameters + ---------- + atom1 : :class:`Atom` or int or str + One of the atoms in the bond. It must be in the ``atoms`` list of + this ResidueTemplate. It can also be the atom index (index from 0) + of the atom in the bond. + atom2 : :class:`Atom` or int or str + The other atom in the bond. It must be in the ``atoms`` list of this + ResidueTemplate. It can also be the atom index (index from 0) of the + atom in the bond. + order : float + The bond order of this bond. Bonds are classified as follows: + 1.0 -- single bond + 2.0 -- double bond + 3.0 -- triple bond + 1.5 -- aromatic bond + 1.25 -- amide bond + Default is 1.0 + + Raises + ------ + IndexError if atom1 or atom2 are integers that are out of range of the + number of atoms already in this template + + RuntimeError if atom1 or atom2 are :class:`Atom` instances but they are + *not* in the atoms list of this ResidueTemplate + + Notes + ----- + If atom1 and atom2 are already bonded, this routine does nothing. If + atom1 or atom2 are strings, then they will match the first instance of + the atom name that is the same as the atom name passed. + """ + if not isinstance(atom1, Atom): + atom1 = self[atom1] + if not isinstance(atom2, Atom): + atom2 = self[atom2] + if atom1.list is not self.atoms or atom2.list is not self.atoms: + raise RuntimeError('Both atoms must belong to template.atoms') + # Do not add the same bond twice + if atom1 not in atom2.bond_partners: + self.bonds.append(Bond(atom1, atom2, order=order)) + + def delete_bond(self, bond): + """ Delete a bond from this residue template. + + Parameters + ---------- + bond : :class:`Bond` + The bond to be deleted + + """ + if bond in self.bonds: + bond.delete() + self.bonds.remove(bond) + else: + raise ValueError(f'The specified bond {bond} does not belong to this residue {self}') + + @classmethod + def from_residue(cls, residue): + """ + This constructor creates a ResidueTemplate from a particular Residue + object + Parameters + ---------- + residue : :class:`Residue` + The residue from which to create a template + """ + inst = cls(name=residue.name) + for atom in residue: + inst.add_atom(_copy.copy(atom)) + for atom in residue: + for bond in atom.bonds: + try: + i1 = residue.atoms.index(bond.atom1) + i2 = residue.atoms.index(bond.atom2) + except ValueError: + if bond.atom1 in residue: + oatom = bond.atom2 + idx = residue.atoms.index(bond.atom1) + else: + oatom = bond.atom1 + idx = residue.atoms.index(bond.atom2) + if oatom.residue.idx == residue.idx - 1: + inst.head = inst.atoms[idx] + elif oatom.residue.idx == residue.idx + 1: + inst.tail = inst.atoms[idx] + elif oatom.residue.idx == residue.idx: + # Don't know WHAT to do with it + warnings.warn('Cannot determine head/tail for unordered residues.') + else: + # Disulfide or something... not head or tail + inst.connections.append(inst.atoms[idx]) + else: + inst.add_bond(i1, i2) + return inst + + @property + def coordinates(self): + """ Atomic coordinates, in Angstroms, of all atoms in the template """ + try: + return self._crd + except AttributeError: + self._crd = np.array([[a.xx, a.xy, a.xz] for a in self]) + return self._crd + + @property + def empirical_chemical_formula(self): + """ Return the empirical chemical formula (in Hill notation) as a string (e.g. 'H2O', 'C6H12'), omitting EPs """ + # Count number of appearances of each element + element_count = defaultdict(int) + for atom in self.atoms: + element_count[atom.element_name] += 1 + # Pop EPs if they are present, since they are not chemical + if 'EP' in element_count: + element_count.pop('EP') + # Render to string using Hill notation + # https://en.wikipedia.org/wiki/Chemical_formula#Hill_system + def format_and_pop_element(element_count, element): + count = element_count.pop(element) + if count == 1: + return element + return element + str(count) + + chemical_formula = '' + # If carbon is present, first list C, then H (if present) + if 'C' in element_count: + chemical_formula += format_and_pop_element(element_count, 'C') + if 'H' in element_count: + chemical_formula += format_and_pop_element(element_count, 'H') + # Remaining elements are listed alphabetically + alphabetical_elements = sorted(element_count.keys()) + for element in alphabetical_elements: + chemical_formula += format_and_pop_element(element_count, element) + + return chemical_formula + + @property + def net_charge(self): + return sum([a.charge for a in self]) + + # Make ResidueTemplate look like a container of atoms, also indexable by the atom name + def __len__(self): + return len(self.atoms) + + def __iter__(self): + return iter(self.atoms) + + def __contains__(self, atom): + if isinstance(atom, Atom): + return atom in self.atoms + if isinstance(atom, str): + return atom in self._map + raise AssertionError('Should not be here!') + + def __copy__(self): + other = type(self)(name=self.name) + + for atom in self.atoms: + other.add_atom(_copy.copy(atom)) + for bond in self.bonds: + other.add_bond(bond.atom1.idx, bond.atom2.idx) + other.type = self.type + + if self.head is not None: + other.head = other.atoms[self.head.idx] + if self.tail is not None: + other.tail = other.atoms[self.tail.idx] + for connection in self.connections: + other.connections.append(other.atoms[connection.idx]) + other.first_patch = self.first_patch + other.last_patch = self.last_patch + + return other + + def __getitem__(self, idx): + if isinstance(idx, str): + for atom in self.atoms: + if atom.name == idx: + return atom + raise IndexError(f'Atom {idx} not found in {self.name}') + elif isinstance(idx, Sequence): + return [self[key] for key in idx] + else: + return self.atoms[idx] + + def fix_charges(self, to=None, precision=4): + """ + Adjusts the partial charge of all atoms in the residue to match the + requested target charge. The default target charge is the closest + integer + + Parameters + ---------- + to : float, optional + The desired net charge of this residue template. Default is the + closest integer charge + precision : int, optional + The number of decimal places that each charge should be rounded to. + Default is 4 + + Returns + ------- + self : :class:`ResidueTemplate` + The current residue template whose charges are being modified + + Notes + ----- + This method modifies the atomic charges of this residue template + in-place. Any residual charge (which is accumulated roundoff beyond the + requested precision) is added to the first atom of the residue. This + will typically be 10^-precision in magnitude, and should almost never be + higher than 2*10^-precision. As long as a reasonable precision is chosen + (no fewer than 3 or 4 decimal places), this will have only a negligible + impact on a force field. + + If provided, "to" will be rounded to the ``precision``'th decimal place + to make sure that the sum of the charges come out as close as possible + to the target charge while still obeying the requested precision. + + Raises + ------ + ValueError + If you try to call fix_charges on a residue template with no atoms + """ + if not self.atoms: + raise ValueError('Cannot fix charges on an empty residue') + net_charge = self.net_charge + if to is None: + to = round(net_charge) + else: + # We need to make sure + to = round(to, precision) + if net_charge == to: + return self + + smear = (to - net_charge) / len(self) + for atom in self: + atom.charge = round(atom.charge + smear, precision) + + # Dump the extra tiny bit (O(10^-precision)) on the first atom + self.atoms[0].charge += to - sum(atom.charge for atom in self.atoms) + + return self + + def apply_patch(self, patch, precision=4): + """ + Apply the specified PatchTemplate to the ResidueTemplate. + + This only handles patches that affect a single residue. + + An exception is thrown if patch is incompatible because + * The patch specifies that an atom is to be deleted that doesn't exist in the residue + * A bond specified as being added in the patch does not have both atom names present after adding/deleting atoms from the patch + * The new net charge is not integral to the specified precision + * The residue is not modified in any way (no atoms or bonds added/changed/deleted) + + Parameters + ---------- + + patch : PatchTemplate + The patch to apply to this residue + + precision : int, optional + Each valid patch should be produce a net charge that is integral to + this many decimal places. + Default is 4 + + Returns + ------- + + residue : ResidueTemplate + A new ResidueTemplate corresponding to the patched residue is returned. + The original remains unmodified. + + """ + # Create a copy + # TODO: Once ResidueTemplate.from_residue() actually copies all info, use that instead? + residue = _copy.copy(self) + # Record whether we've actually modified the residue. + modifications_made = False + # Delete atoms + for atom_name in patch.delete_atoms: + try: + residue.delete_atom(atom_name) + modifications_made = True + except (KeyError, MoleculeError) as err: + if atom_name.startswith('D') and atom_name[1:] in self and atom_name[1:] in patch.delete_atoms: + # This is a Drude particle. We're also deleting its parent atom, so don't report an error. + pass + else: + raise IncompatiblePatchError( + f'Atom {atom_name} could not be deleted from the patched residue: atoms ' + f'are {list(residue._map.keys())} (exception: {err})' + ) from err + # Add or replace atoms + for atom in patch.atoms: + if atom.name in residue: + # Overwrite type and charge + residue[atom.name].type = atom.type + residue[atom.name].charge = atom.charge + else: + residue.add_atom(Atom(name=atom.name, type=atom.type, charge=atom.charge)) + modifications_made = True + # Add bonds + for (atom1_name, atom2_name, order) in patch.add_bonds: + try: + # Remove dangling bonds + for name in [atom1_name, atom2_name]: + if residue.head and (name == residue.head.name): + residue.head = None + if residue.tail and (name == residue.tail.name): + residue.tail = None + # Add bond + residue.add_bond(atom1_name, atom2_name, order) + modifications_made = True + except (IndexError, MoleculeError) as err: + raise IncompatiblePatchError( + f'Bond {atom1_name}-{atom2_name} could not be added to patched residue: ' + f'atoms are {list(residue._map.keys())} (exception: {err})' + ) from err + # Delete impropers + for impr in patch.delete_impropers: + try: + residue._impr.remove(impr) + # removal of impropers doesn't do anything as far as OpenMM is concerned, so don't note this as a modification having been made + except ValueError as e: + raise IncompatiblePatchError(f'Improper {impr} was not found in residue to be patched.') + # Check that the net charge is integral. + net_charge = residue.net_charge + is_integral = (round(net_charge, precision) - round(net_charge)) == 0.0 + if not is_integral: + raise IncompatiblePatchError(f'Patch is not compatible with residue due to non-integral charge (charge was {net_charge}).') + # Ensure residue is connected + import networkx as nx + G = residue.to_networkx(False) + if nx.is_empty(G): + raise IncompatiblePatchError('Patch creates empty residue.') + if not nx.is_connected(G): + components = [ c for c in nx.connected_components(G) ] + raise IncompatiblePatchError(f'Patched residue bond graph is not a connected graph: {components}') + # Make sure the patch has actually modified the residue + if not modifications_made: + raise IncompatiblePatchError('Patch did not modify residue.') + + return residue + + def patch_is_compatible(self, patch): + """Determine whether a specified patch is compatible with this residue. + + Compatibility is determined by whether Residue.Template.apply_patch(patch) raises as + exception or not. + + Parameters + ---------- + patch : PatchTemplate + The patch to be applied to this residue. + + Returns + ------- + is_compatible : bool + True if patch is compatible with the residue; False if not. + + """ + try: + self.apply_patch(patch) + return True + except IncompatiblePatchError: + return False + + def to_networkx(self, include_extra_particles=True): + """ Create a NetworkX graph of atoms and bonds + + Parameters + ---------- + include_extra_particles : bool + Whether to include "atoms" that actually represent extra particles (atomic_number == 0). + + Returns + ------- + G : :class:`networkx.Graph` + A NetworkX Graph representing the molecule + + """ + import networkx + G = networkx.Graph() + for atom in self.atoms: + if atom.atomic_number != 0 or include_extra_particles: + G.add_node(atom.name, charge=atom.charge, type=atom.type) + for bond in self.bonds: + if (bond.atom1.atomic_number != 0 and bond.atom2.atomic_number != 0) or include_extra_particles: + G.add_edge(bond.atom1.name, bond.atom2.name) + return G + + def to_dataframe(self): + """ Create a pandas dataframe from the atom information + + Returns + ------- + df : :class:`pandas.DataFrame` + The pandas DataFrame with all of the atomic properties + + Notes + ----- + The DataFrame will be over all atoms. The columns will be the attributes + of the atom (as well as its containing residue). Some columns will + *always* exist. Others will only exist if those attributes have been set + on the Atom instances (see the :class:`Atom` docs for possible + attributes and their meaning). The columns that will always be present + are: + + - number : int + - name : str + - type : str + - atomic_number : int + - charge : float + - mass : float + - nb_idx : int + - solvent_radius : float + - screen : float + - occupancy : float + - bfactor : float + - altloc : str + - tree : str + - join : int + - irotat : int + - rmin : float + - epsilon : float + - rmin_14 : float + - epsilon_14 : float + + The following attributes are optionally present if they were present in + the original file defining the structure: + + - xx : float (x-coordinate position) + - xy : float (y-coordinate position) + - xz : float (z-coordinate position) + - vx : float (x-coordinate velocity) + - vy : float (y-coordinate velocity) + - vz : float (z-coordinate velocity) + """ + import pandas as pd + ret = pd.DataFrame() + + ret['number'] = [atom.number for atom in self.atoms] + ret['name'] = [atom.name for atom in self.atoms] + ret['type'] = [atom.type for atom in self.atoms] + ret['atomic_number'] = [atom.atomic_number for atom in self.atoms] + ret['charge'] = [atom.charge for atom in self.atoms] + ret['mass'] = [atom.mass for atom in self.atoms] + ret['nb_idx'] = [atom.nb_idx for atom in self.atoms] + ret['solvent_radius'] = [atom.solvent_radius for atom in self.atoms] + ret['screen'] = [atom.screen for atom in self.atoms] + ret['occupancy'] = [atom.occupancy for atom in self.atoms] + ret['bfactor'] = [atom.bfactor for atom in self.atoms] + ret['altloc'] = [atom.altloc for atom in self.atoms] + ret['tree'] = [atom.tree for atom in self.atoms] + ret['join'] = [atom.join for atom in self.atoms] + ret['irotat'] = [atom.irotat for atom in self.atoms] + ret['rmin'] = [atom.rmin for atom in self.atoms] + ret['epsilon'] = [atom.epsilon for atom in self.atoms] + ret['rmin_14'] = [atom.rmin_14 for atom in self.atoms] + ret['epsilon_14'] = [atom.epsilon_14 for atom in self.atoms] + ret['resname'] = [atom.residue.name for atom in self.atoms] + + # Now for optional attributes + # Coordinates + try: + coords = pd.DataFrame( + [[atom.xx, atom.xy, atom.xz] for atom in self.atoms], columns=['xx', 'xy', 'xz'] + ) + except AttributeError: + pass + else: + ret = ret.join(coords) + # Velocities + try: + vels = pd.DataFrame( + [[atom.vx, atom.vy, atom.vz] for atom in self.atoms], columns=['vx', 'vy', 'vz'] + ) + except AttributeError: + pass + else: + ret = ret.join(vels) + return ret + + def to_structure(self): + """ + Generates a Structure instance with a single residue from this + ResidueTemplate + + Returns + ------- + struct : :class:`parmed.structure.Structure` + The Structure with all of the bonds and connectivity of this + template + """ + from ..structure import Structure + struct = Structure() + for atom in self: + struct.add_atom(_copy.copy(atom), self.name, 0) + for bond in self.bonds: + struct.bonds.append(Bond(struct.atoms[bond.atom1.idx], struct.atoms[bond.atom2.idx])) + return struct + + def save(self, fname, format=None, overwrite=False, **kwargs): + """ + Saves the current ResidueTemplate in the requested file format. + Supported formats can be specified explicitly or determined by file-name + extension. The following formats are supported, with the recognized + suffix shown in parentheses: + + - MOL2 (.mol2) + - MOL3 (.mol3) + - OFF (.lib/.off) + - PDB (.pdb) + - PQR (.pqr) + + Parameters + ---------- + fname : str + Name of the file to save. If ``format`` is ``None`` (see below), the + file type will be determined based on the filename extension. If the + type cannot be determined, a ValueError is raised. + format : str, optional + The case-insensitive keyword specifying what type of file ``fname`` + should be saved as. If ``None`` (default), the file type will be + determined from filename extension of ``fname`` + overwrite : bool, optional + If True, allow the target file to be overwritten. Otherwise, an + IOError is raised if the file exists. Default is False + kwargs : keyword-arguments + Remaining arguments are passed on to the file writing routines that + are called by this function + + Raises + ------ + ValueError if either filename extension or ``format`` are not recognized + TypeError if the structure cannot be converted to the desired format for + whatever reason + """ + from ..amber.offlib import AmberOFFLibrary + from ..formats.mol2 import Mol2File + extmap = { + '.mol2' : 'MOL2', + '.mol3' : 'MOL3', + '.off' : 'OFFLIB', + '.lib' : 'OFFLIB', + '.pdb' : 'PDB', + '.pqr' : 'PQR', + } + if format is not None: + format = format.upper() + else: + base, ext = os.path.splitext(fname) + if ext in ('.bz2', '.gz'): + ext = os.path.splitext(base)[1] + if ext in extmap: + format = extmap[ext] + else: + raise ValueError(f'Could not determine file type of {fname}') + if format == 'MOL2': + Mol2File.write(self, fname, mol3=False, **kwargs) + elif format == 'MOL3': + Mol2File.write(self, fname, mol3=True, **kwargs) + elif format in ('OFFLIB', 'OFF'): + AmberOFFLibrary.write({self.name : self}, fname, **kwargs) + elif format in ('PDB', 'PQR'): + self.to_structure().save(fname, format=format, overwrite=overwrite, **kwargs) + else: + raise ValueError('Unrecognized format for ResidueTemplate save') + +class PatchTemplate(ResidueTemplate): + """ + A residue patch (typically used for CHARMM) that is used to modify existing + residues in some way (e.g., terminal patches, disulfide bridges, etc.) + + Parameters + ---------- + name : str, optional + If provided, this is the name of the residue + + Attributes + ---------- + add_bonds : list of (str, str, order) + List of bonds that need to be added in applying the patch + delete_atoms : list of str + List of atom names that need to be deleted in applying the patch + delete_impropers : list of tuple of str + List of impropers (tuple of atom names) that need to be deleted in applying the patch + + See Also + -------- + :class:`ResidueTemplate` + + Notes + ----- + This class basically just provides an additional list of atoms that need to + be deleted when applying this patch -- something that does not apply to + standard Residues + """ + def __init__(self, name=''): + super().__init__(name) + self.add_bonds = [] + self.delete_atoms = [] + self.delete_impropers = [] + +class ResidueTemplateContainer(list): + """ + A container of ResidueTemplate objects representing a unit with multiple + residues + + Parameters + ---------- + name : str, optional + The name of the residue container + """ + def __init__(self, name=''): + self.box = None + self.name = name + + @classmethod + def from_structure(cls, struct, term_decorate=True): + """ + Instantiates a ResidueTemplateContainer from a Structure instance filled + with residues + + Parameters + ---------- + struct : :class:`parmed.structure.Structure` + The structure from which to generate the ResidueTemplateContainer + from + term_decorate : bool, optional + If True, terminal amino and nucleic acid residues will be adorned as + follows: + + * N-prepended if it is an N-terminal amino acid + * C-prepended if it is a C-terminal amino acid + * 5-appended if it is a 5'-terminal nucleic acid + * 3-appended if it is a 3'-terminal nucleic acid + + For example, an N-terminal GLY will become NGLY, while a 5'-terminal + DA will become DA5. Default is True + """ + inst = cls() + for res in struct.residues: + rt = ResidueTemplate.from_residue(res) + # See if we need to decorate the termini names + if rt.head is None and rt.tail is not None and term_decorate: + if AminoAcidResidue.has(rt.name): + if len(rt.name) != 4 or rt.name[0] != 'N': + rt.name = f'N{rt.name}' + elif RNAResidue.has(rt.name) or DNAResidue.has(rt.name): + if rt.name[-1] != '5': + rt.name = f'{rt.name}5' + elif rt.tail is None and rt.head is not None and term_decorate: + if AminoAcidResidue.has(rt.name): + if len(rt.name) != 4 or rt.name[0] != 'C': + rt.name = f'C{rt.name}' + elif RNAResidue.has(rt.name) or DNAResidue.has(rt.name): + if rt.name[-1] != '3': + rt.name = f'{rt.name}3' + inst.append(rt) + inst.box = struct.box + return inst + + def __getitem__(self, value): + if isinstance(value, str): + # Behave like a dict here... albeit a slow one + for res in self: + if res.name == value: return res + return list.__getitem__(self, value) + + def fix_charges(self, precision=4): + """ + Adjusts the net charge of all residues in this ResidueContainer to match + the closest integer charge + + Parameters + ---------- + precision : int, optional + The number of decimal places that each charge should be rounded to. + Default is 4 + + Returns + ------- + self : :class:`ResidueTemplateContainer` + The current residue template container whose ResidueTemplates are + being modified + + Notes + ----- + This method modifies everything in-place. + + Raises + ------ + ValueError + If you try to call fix_charges on a container with no templates + """ + if len(self) == 0: + raise ValueError('Cannot fix charges on an empty container') + for res in self: + res.fix_charges(precision=precision) + return self + + def to_library(self): + """ + Converts the ResidueTemplateContainer instance to a library of unique + :class:`ResidueTemplate` instances. The first of each kind of residue is + taken + + Returns + ------- + residues : dict {str : :class:`ResidueTemplate`} + The residue library with all residues from this residue collection + """ + ret = OrderedDict() + for res in self: + if res.name in ret: + continue + ret[res.name] = res + return ret + + @classmethod + def from_library(cls, library, copy=False): + """ + Converts a dictionary of ResidueTemplate items into a + ResidueTemplateContainer. + + Parameters + ---------- + library : dict or OrderedDict + The library of ResidueTemplate objects to add to this container + copy : bool, optional + If True, copies of each ResidueTemplate in library is added to the + ResidueTemplateContainer. Default is False + + Returns + ------- + cont : ResidueTemplateContainer + A ResidueTemplateContainer containing all of the residues defined in + ``library`` + + Notes + ----- + If the library is ordered, that order is maintained + + Raises + ------ + TypeError if any of the items in the input library is not a + ResidueTemplate instance (or an instance of a subclass) + """ + cont = cls() + for _, res in library.items(): + if not isinstance(res, ResidueTemplate): + raise ValueError(f'{res} is not a ResidueTemplate instance') + if copy: + cont.append(_copy.copy(res)) + else: + cont.append(res) + return cont + + def save(self, fname, format=None, **kwargs): + """ + Saves the current ResidueTemplateContainer in the requested file format. + Supported formats can be specified explicitly or determined by file-name + extension. The following formats are supported, with the recognized + suffix and ``format`` keyword shown in parentheses: + + - MOL2 (.mol2) + - MOL3 (.mol3) + - OFF (.lib/.off) + + Parameters + ---------- + fname : str + Name of the file to save. If ``format`` is ``None`` (see below), the + file type will be determined based on the filename extension. If the + type cannot be determined, a ValueError is raised. + format : str, optional + The case-insensitive keyword specifying what type of file ``fname`` + should be saved as. If ``None`` (default), the file type will be + determined from filename extension of ``fname`` + kwargs : keyword-arguments + Remaining arguments are passed on to the file writing routines that + are called by this function + + Raises + ------ + ValueError if either filename extension or ``format`` are not recognized + TypeError if the structure cannot be converted to the desired format for + whatever reason + + Notes + ----- + Mol2 and Mol3 files are saved as concatenated multiple @s. By + contrast, ``Structure.save`` will save a single @ mol2 file + with multiple residues if the mol2 format is requested. + """ + from ..amber.offlib import AmberOFFLibrary + from ..formats.mol2 import Mol2File + extmap = { + '.mol2' : 'MOL2', + '.mol3' : 'MOL3', + '.off' : 'OFFLIB', + '.lib' : 'OFFLIB', + } + if format is not None: + format = format.upper() + else: + base, ext = os.path.splitext(fname) + if ext in ('.bz2', '.gz'): + ext = os.path.splitext(base)[1] + if ext in extmap: + format = extmap[ext] + else: + raise ValueError(f'Could not determine file type of {fname}') + if format == 'MOL2': + Mol2File.write(self, fname, mol3=False, split=True, **kwargs) + elif format == 'MOL3': + Mol2File.write(self, fname, mol3=True, split=True, **kwargs) + elif format in ('OFFLIB', 'OFF'): + AmberOFFLibrary.write(self.to_library(), fname, **kwargs) + else: + raise ValueError('Unrecognized format for ResidueTemplate save') diff --git a/pmx/workflow/parmed/modeller/standardtemplates.py b/pmx/workflow/parmed/modeller/standardtemplates.py new file mode 100644 index 00000000..5ec41aec --- /dev/null +++ b/pmx/workflow/parmed/modeller/standardtemplates.py @@ -0,0 +1,10 @@ +""" Standard residue templates for biomolecular residues """ + +import os +from ..amber.offlib import AmberOFFLibrary + +__all__ = ['StandardBiomolecularResidues'] + +StandardBiomolecularResidues = AmberOFFLibrary.parse( + os.path.join(os.path.split(__file__)[0], 'data', 'standard_residues.lib') +) diff --git a/pmx/workflow/parmed/namd/__init__.py b/pmx/workflow/parmed/namd/__init__.py new file mode 100644 index 00000000..2b939497 --- /dev/null +++ b/pmx/workflow/parmed/namd/__init__.py @@ -0,0 +1,12 @@ +""" +This package contains code for reading, writing, and modifying NAMD files +needed to start and/or restart simulations with NAMD. The most common formats +are the so-called "namdbin" files heavily utilized by VMD; these are often +given the extensions .coor and .vel (coordinates and velocities, respectively). +""" +__all__ = ['NamdBinCoor', 'NamdBinVel'] +__authors__ = 'Brian Radak' +__license__ = 'GPL v.3' + + +from .namdbinfiles import NamdBinCoor, NamdBinVel diff --git a/pmx/workflow/parmed/namd/namdbinfiles.py b/pmx/workflow/parmed/namd/namdbinfiles.py new file mode 100644 index 00000000..191b67df --- /dev/null +++ b/pmx/workflow/parmed/namd/namdbinfiles.py @@ -0,0 +1,110 @@ +""" +This module contains classes for reading and writing (single frame) NAMD binary +files. The main functionality is currently aimed at manipulation rather than +analysis. +""" +from .. import unit as u +from struct import unpack, pack + +import numpy as np + +class NamdBinFile: + """From the NAMD manual: + + NAMD uses a trivial double-precision binary file format for + coordinates, velocities, and forces ... The file consists of the atom + count as a 32-bit integer followed by all three position or velocity + components for each atom as 64-bit double-precision floating point ... + + The main attributes are the number of atom entries (natom) and a (flat) + numpy array of size 3*natom (values). The meaning of "values" is + effectively arbitrary, but for convenience derived classes are provided + which alias the values to more descriptive names (e.g. "coordinates"). + + See Also + -------- + :class:`NamdBinCoor` + :class:`NamdBinVel` + """ + _SCALE_FACTOR = 1.0 + def __init__(self, values=[]): + self._values = np.asarray(values,np.float64) * self._SCALE_FACTOR + + @property + def natom(self): + """The current number of atom entries.""" + return int(self._values.size / 3) + + @classmethod + def read(cls, fname): + """Return an object from the values in a NAMD binary file.""" + infile = open(fname,'rb') + natoms = int(unpack('i',infile.read(4))[0]) + values = [unpack('d',infile.read(8))[0] for n in range(3*natoms)] + infile.close() + return cls(values) + + def write(self, fname): + """Write the current attributes to a file.""" + outfile = open(fname,'wb') + outfile.write(pack('i',self.natom)) + for x in self._values / self._SCALE_FACTOR: + outfile.write(pack('d',x)) + outfile.close() + + def delatoms(self, indices): + """Delete entries corresponding to the given atom indices.""" + del_indices = np.atleast_1d(np.asarray(indices,np.int32)) + mask = np.ones(self.natom,np.bool) + mask[del_indices] = False + newvalues = np.zeros(3*(self.natom - del_indices.size),np.float64) + newvalues += self._values.reshape((self.natom,3))[mask].flatten() + self._values = newvalues + + def insertatoms(self, start_index, natoms, values=None): + """Insert space for natom entries beginning at start_index. If + specified, give them the provided values, otherwise set them to zero. + """ + if values is not None: + values = np.asarray(values) + assert values.size == 3*natoms + else: + values = np.zeros(3*natoms) + hinge = 3*start_index + newvalues = np.concatenate((self._values[:hinge],values,self._values[hinge:])) + self._values = newvalues + + def copyatoms(self, start_index, natoms): + """Convenience function, same as insertatoms() but set 'values' to + be the same as the previous natoms' values (i.e. make a copy of them). + """ + values = self._values[3*(start_index-natoms):3*start_index] + self.insertatoms(start_index,natoms,values) + + +class NamdBinCoor(NamdBinFile): + """ Class to read or write NAMD "bincoordinates" files. """ + @property + def coordinates(self): + return self._values.reshape((-1, self.natom, 3)) + + @coordinates.setter + def coordinates(self, value): + if u.is_quantity(value): + value = value.value_in_unit(u.angstroms) + self._values = np.array(value).flatten() + +class NamdBinVel(NamdBinFile): + """ Class to read or write NAMD "binvelocities" files. """ + + _SCALE_FACTOR = 20.45482706 + + @property + def velocities(self): + return self._values.reshape((-1, self.natom, 3)) + + @velocities.setter + def velocities(self, value): + if u.is_quantity(value): + value = value.value_in_unit(u.angstroms/u.picosecond) + self._values = np.array(value).flatten() diff --git a/pmx/workflow/parmed/openmm/__init__.py b/pmx/workflow/parmed/openmm/__init__.py new file mode 100644 index 00000000..d74eb76b --- /dev/null +++ b/pmx/workflow/parmed/openmm/__init__.py @@ -0,0 +1,17 @@ +""" +This is a collection of all of the OpenMM functionality supported in ParmEd +""" + +__all__ = ['StateDataReporter', 'NetCDFReporter', 'MdcrdReporter', + 'RestartReporter', 'ProgressReporter', 'EnergyMinimizerReporter', + 'utils', 'load_topology', 'XmlFile', 'energy_decomposition', + 'energy_decomposition_system', 'OpenMMParameterSet'] + +from .reporters import ( + StateDataReporter, NetCDFReporter, MdcrdReporter, RestartReporter, + ProgressReporter, EnergyMinimizerReporter, +) +from .parameters import OpenMMParameterSet +from .topsystem import load_topology +from .utils import energy_decomposition, energy_decomposition_system +from .xmlfile import XmlFile diff --git a/pmx/workflow/parmed/openmm/parameters.py b/pmx/workflow/parmed/openmm/parameters.py new file mode 100644 index 00000000..2b19f5c2 --- /dev/null +++ b/pmx/workflow/parmed/openmm/parameters.py @@ -0,0 +1,1208 @@ +""" +This module contains the class for storing and creating/converting/writing +OpenMM-style ffxml files defining a force field +""" +import datetime +import math +import warnings +from collections import OrderedDict +from collections.abc import Sequence, Mapping +from contextlib import closing +from copy import copy +from functools import wraps +from itertools import product, chain + +from ..charmm.parameters import CharmmImproperMatchingMixin +from ..constants import DEFAULT_ENCODING +from ..formats.registry import FileFormatType +from ..modeller.residue import ResidueTemplate, PatchTemplate +from ..parameters import ParameterSet +from ..periodic_table import Element +from ..topologyobjects import NoUreyBradley +from .. import unit as u +from ..utils.io import genopen +from ..exceptions import ParameterWarning +from ..topologyobjects import DihedralType, ImproperType, DrudeAtom + +try: + from lxml import etree + _have_lxml = True + def pretty_print(tree): + return etree.tostring(tree, encoding=DEFAULT_ENCODING, pretty_print=True).decode('utf-8') +except ImportError: + from xml.dom import minidom + from xml.etree import ElementTree as etree + _have_lxml = False + def pretty_print(tree): + xml = etree.tostring(tree.getroot(), encoding=DEFAULT_ENCODING).decode('utf-8') + return minidom.parseString(xml).toprettyxml(indent=" ") + +import logging +LOGGER = logging.getLogger(__name__) + +class OpenMMParameterSet(ParameterSet, CharmmImproperMatchingMixin, metaclass=FileFormatType): + """ Class storing parameters from an OpenMM parameter set + + Parameters + ---------- + filenames : str, list of str, file-like, or list of file-like; optional + Either the name of a file or a list of filenames from which parameters + should be parsed. + + Notes + ----- + Order is important in the list of files provided. The parameters are loaded + in the order they are provided, and any parameters that are specified in + multiple places are overwritten (that is, the *last* occurrence is the + parameter type that is used) + + See Also + -------- + :class:`parmed.parameters.ParameterSet` + """ + + @staticmethod + def id_format(filename): + """ + Identifies the file type as either an Amber-style frcmod or parm.dat + file. + + Parameters + ---------- + filename : str + Name of the file to check format for + + Returns + ------- + is_fmt : bool + True if it is an Amber-style parameter file. False otherwise. + """ + # Not implemented yet + return False + + def __init__(self, *filenames): + super(OpenMMParameterSet, self).__init__() + if filenames: + raise NotImplementedError('Cannot yet read OpenMM Parameter sets') + self.unique_atom_types = False + + @classmethod + def _remediate_residue_template(cls, params, residue): + """ + Modify non-compliant residue templates to conform with OpenMM requirements. + + * To correctly detect waters, OpenMM ffxml water models must not contain + non-chemical bond constraints. Theses are removed when importing + foreign parameter sets (e.g., CHARMM) into OpenMM parameter sets, + and not restored on conversion from OpenMM to other formats + + Parameters + ---------- + params : :class:`parmed.parameters.ParameterSet` + ParameterSet containing the list of parameters to be converted to a + OpenMM-compatible parameter set + residue : :class:`parmed.modeller.Residue` + The residue to remediate + + Returns + ------- + missing_parameters : bool + If True, the residue template is missing some parameters + + """ + # Populate atomic numbers in residue template + # TODO: This can be removed if the parameter readers are guaranteed to populate this correctly + for atom in residue.atoms: + if atom.type not in params.atom_types_str: + warnings.warn('Residue {} contains atom type {} not found in parameter set and will be dropped.'.format(residue.name, atom.type)) + return False + atom.atomic_number = params.atom_types_str[atom.type].atomic_number + + # CHARMM Drude force field lists all lone pairs as being hydrogens??? Fix them. + types = dict((atom.name, atom.type) for atom in residue.atoms) + for lonepair in residue.lonepairs: + lp_atom = lonepair[1] + params.atom_types[types[lp_atom]].atomic_number = 0 + residue[lp_atom].atomic_number = 0 + + # CHARMM Drude force field includes bonds to lone pairs. Delete them. + # The call to list() makes a copy of the list, so we don't modify a list + # we're iterating over. + for bond in list(residue.bonds): + if (bond.atom1.atomic_number == 0) or (bond.atom2.atomic_number == 0): + LOGGER.debug(f'Deleting bonds to virtual sites in residue {residue.name}') + residue.delete_bond(bond) + + # Check waters + if residue.empirical_chemical_formula == 'H2O': + # Remove any H-H bonds if they are present + for bond in list(residue.bonds): + if (bond.atom1.element_name == 'H') and (bond.atom2.element_name == 'H'): + # Remove nonphysical H-H bonds + LOGGER.debug(f'Deleting H-H bond from water residue {residue.name}') + residue.delete_bond(bond) + else: + LOGGER.debug(f'keeping {bond.atom1} to {bond.atom2} {bond.atom2.element_name}') + return True + + @classmethod + def from_parameterset(cls, params, copy=False, remediate_residues=True, unique_atom_types=False): + """ + Instantiates an OpenMMParameterSet from another ParameterSet (or + subclass). The main thing this feature is responsible for is converting + lower-case atom type names into all upper-case and decorating the name + to ensure each atom type name is unique. + + Warning + ------- + Converting parameter sets to OpenMM can be lossy, and can modify the + original parameter set unless ``copy=True``: + * To correctly detect waters, OpenMM ffxml water models must not contain + non-chemical bond constraints. Theses are removed when importing + foreign parameter sets (e.g., CHARMM) into OpenMM parameter sets, + and not restored on conversion from OpenMM to other formats. + + Parameters + ---------- + params : :class:`parmed.parameters.ParameterSet` + ParameterSet containing the list of parameters to be converted to as + OpenMM-compatible parameter set + copy : bool, optional, default=False + If True, the returned parameter set is a deep copy of ``params``. If + False, the returned parameter set is a shallow copy. Default False. + remediate_residues : bool, optional, default=True + If True, will remove non-chemical bonds and drop Residue definitions + that are missing parameters + unique_atom_types : bool + If True, a unique OpenMM atom type will be created for every atom of + every residue. In this case, the :class:`AtomType` objects correspond + to atom classes rather than atom types (in the OpenMM terminology). + + Returns + ------- + new_params : OpenMMParameterSet + OpenMMParameterSet with the same parameters as that defined in the + input parameter set + """ + new_params = cls() + if copy: + # Make a copy so we don't modify the original + params = copy(params) + + new_params.atom_types = new_params.atom_types_str = params.atom_types + new_params.atom_types_int = params.atom_types_int + new_params.atom_types_tuple = params.atom_types_tuple + new_params.bond_types = params.bond_types + new_params.angle_types = params.angle_types + new_params.urey_bradley_types = params.urey_bradley_types + new_params.dihedral_types = params.dihedral_types + new_params.improper_types = params.improper_types + new_params.improper_periodic_types = params.improper_periodic_types + new_params.rb_torsion_types = params.rb_torsion_types + new_params.cmap_types = params.cmap_types + new_params.nbfix_types = params.nbfix_types + new_params.pair_types = params.pair_types + new_params.parametersets = params.parametersets + new_params._combining_rule = params.combining_rule + new_params.default_scee = params.default_scee + new_params.default_scnb = params.default_scnb + new_params.unique_atom_types = unique_atom_types + + # Copy CHARMM improper type map, if present, since this is needed for matching impropers + if hasattr(params, '_improper_key_map'): + new_params._improper_key_map = new_params._improper_key_map + + if remediate_residues: + # Add only ResidueTemplate instances (no ResidueTemplateContainers) + # Maintain original residue ordering + remediated_residues = list() + for name, residue in params.residues.items(): + if isinstance(residue, ResidueTemplate): + # Don't discard the residue, but fix it if we need to + cls._remediate_residue_template(new_params, residue) + remediated_residues.append(residue) + for residue in remediated_residues: + new_params.residues[residue.name] = residue + for name, patch in params.patches.items(): + cls._remediate_residue_template(new_params, patch) + new_params.patches[patch.name] = patch + else: + # Don't remediate residues; just copy + for name, residue in params.residues.items(): + new_params.residues[residue.name] = residue + for name, patch in params.patches.items(): + new_params.patches[patch.name] = patch + + # Only add unique patches + unique_patches = OrderedDict() + discarded_patches = [] + for name, patch in params.patches.items(): + if isinstance(patch, PatchTemplate): + templhash = cls._templhasher(patch) + if templhash not in unique_patches: + new_params.patches[name] = patch + unique_patches[templhash] = patch + else: + patch_collision = unique_patches[templhash] + warnings.warn(f'Patch {patch} discarded because OpenMM considers it identical to {patch_collision}') + discarded_patches.append(patch) + + if (len(discarded_patches) > 0): + warnings.warn(f'{len(discarded_patches)} patches discarded, {len(new_params.patches)} retained') + for patch in discarded_patches: + del new_params.patches[patch.name] + + return new_params + + def _get_mm_atom_type(self, atom, residue, drude=False): + """Get the OpenMM atom type for an atom. + + Parameters + ---------- + atom : :class:`Atom` + the atom for which to get the type + residue : :class:`ResidueTemplate` or :class:`PatchTemplate` + the residue the atom belongs to + drude : bool + if True, get the atom type for the Drude particle attached to the + atom rather than the atom itself + """ + if self.unique_atom_types: + if drude: + return f'Drude-{residue.name}-{atom.name}' + return f'{residue.name}-{atom.name}' + if drude: + return atom.drude_type + return atom.type + + def write(self, dest, provenance=None, write_unused=True, separate_ljforce=False, + improper_dihedrals_ordering='default', charmm_imp=False, skip_duplicates=True, is_glycam=False): + """ Write the parameter set to an XML file for use with OpenMM + + Parameters + ---------- + dest : str or file-like + The name of the file or the file-like object (with a ``write`` + attribute) to which the XML file will be written + provenance : dict, optional + If present, the XML file will be tagged with the available fields. + Keys of the dictionary become XML etree.Element tags, the values of the + dictionary must be instances of any of: + - str / unicode (Py2) or str (Py3) - one XML element with this + content is written + - list - one XML element per each item of the list is written, all + these XML elements use the same tag (key in provenance dict) + - dict - one of the keys of this dict must be the same as the key of + of the provenance dict under which this dict is nested. The value + under this key becomes the content of the XML element. Remaining keys + and their values are used to construct attributes of the XML element. + Note that OrderedDict's should be used to ensure appropriate order + of the XML elements and their attributes. + Default is no provenance. + Example (unordered): + provenance = {'Reference' : ['Nature', 'Cell'], + 'Source' : {'Source': 'leaprc.ff14SB', sourcePackage : + 'AmberTools', sourcePackageVersion : '15'}, + 'User' : 'Mark'} + write_unused : bool + If False: a) residue templates using unavailable atom types will not + be written, b) atom types that are not used in any of the residue + templates remaining and parameters including those atom types will + not be written. A ParameterWarning is issued if any such residues are + found in a). + separate_ljforce : bool + If True will use a separate LennardJonesForce to create a + CostumNonbondedForce to compute L-J interactions. It will set sigma + to 1 and epsilon to 0 in the NonbondedForce so that the + NonbondedForce only calculates the electrostatic contribution. It + should be set to True when converting a CHARMM force field file that + doesn't have pair-specific L-J modifications (NBFIX in CHARMM) so + that the ffxml conversion is compatible with the main charmm36.xml file. + Note: + ---- + When pair-specific L-J modifications are present (NBFIX in CHARMM), this + behavior is always present and this flag is ignored. + improper_dihedrals_ordering : str + The ordering to use when assigning improper torsions in OpenMM. Default is 'default', + other option is 'amber' + charmm_imp: bool + If True, will check for existence of IMPR in each residue and patch template, + and write out the explicit improper definition without wildcards in the ffxml file. + skip_duplicates : bool + If True: residues which appear identical to an existing residue will + not be written. This is usually the best choice. The most common + reason for setting skip_duplicates to false is if you have different + parametrizations for stereoisomers. Note that since OpenMM's residue + hashing is not aware of chirality, if you wish to use the results in + simulations you will need to explicitly provide the template names + for affected residues. + + Notes + ----- + The generated XML file will have the XML tag ``DateGenerated`` added to + the provenance information set to the current date. Therefore, you + should not provide this information in ``provenance`` (it will be + removed if it is provided). + """ + if not write_unused: + skip_residues = self._find_unused_residues() + skip_types = self._find_unused_types(skip_residues) + if skip_residues: + warnings.warn('Some residue templates using unavailable AtomTypes ' + 'were found. They will not be written to the ffxml ' + 'as write_unused is set to False', ParameterWarning) + else: + skip_residues = set() + skip_types = set() + if self.atom_types: + try: + self.typeify_templates() + except KeyError: + warnings.warn('Some residue templates are using unavailable AtomTypes', + ParameterWarning) + + [valid_residues_for_patch, valid_patches_for_residue] = self._determine_valid_patch_combinations(skip_residues) + LOGGER.debug('Valid patch combinations:') + for patch_name in self.patches: + LOGGER.debug(f'{patch_name:8s} : {valid_residues_for_patch[patch_name]}') + + if charmm_imp: + self._find_explicit_impropers() + + self._compress_impropers() + + root = etree.Element('ForceField') + self._write_omm_provenance(root, provenance) + self._write_omm_atom_types(root, skip_types, skip_residues) + self._write_omm_residues(root, skip_residues, skip_duplicates, + valid_patches_for_residue=valid_patches_for_residue, is_glycam=is_glycam) + self._write_omm_patches(root, valid_residues_for_patch) + self._write_omm_bonds(root, skip_types) + self._write_omm_angles(root, skip_types) + self._write_omm_urey_bradley(root, skip_types) + self._write_omm_dihedrals(root, skip_types, improper_dihedrals_ordering) + self._write_omm_impropers(root, skip_types) + #self._write_omm_rb_torsions(root, skip_types) + self._write_omm_cmaps(root, skip_types) + self._write_omm_scripts(root, skip_types) + self._write_omm_nonbonded(root, skip_types, separate_ljforce) + self._write_omm_LennardJonesForce(root, skip_types, separate_ljforce) + self._write_omm_DrudeForce(root, skip_types) + + tree = etree.ElementTree(root) + + xml = pretty_print(tree) + + if isinstance(dest, str): + with closing(genopen(dest, 'w')) as f: + f.write(xml) + else: + dest.write(xml) + + def _find_explicit_impropers(self): + """ + For every residue, find any explicitly-specified (e.g. CHARMM) improper torsions and identify all wild-card improper parameters that match. + Expand all of these out into explicit impropers. + This is necessary for OpenMM to correctly handle impropers for these residues. + + .. todo :: + + * Do we need to do this for patches as well? + + """ + + # Regenerate improper key map + self._improper_key_map = OrderedDict() + for key in self.improper_types.keys(): + self._improper_key_map[tuple(sorted(key))] = key + + improper_harmonic = OrderedDict() # improper_harmonic[key] is the harmonic improper parameter for unique key `key` + improper_periodic = OrderedDict() # improper_harmonic[key] is the periodic improper parameter for unique key `key` + C_types = [t for t in self.atom_types if self.atom_types[t].atomic_number == 6] + N_types = [t for t in self.atom_types if self.atom_types[t].atomic_number == 7] + + def get_types(residue, atomname): + """Return list of atom type(s) that match the given atom name. + """ + a_names = [a.name for a in residue.atoms] + a_types = [a.type for a in residue.atoms] + + if atomname == '-C': + return C_types + elif atomname == '+N': + return N_types + elif atomname[0] in ['-', '+']: + raise ValueError(f'Unknown atom name {atomname}') + else: + return [a_types[a_names.index(atomname)]] + + # Iterate over all residues + for name, residue in chain(self.residues.items(), self.patches.items()): + for impr in residue._impr: + # Get the list of types involved in this improper + try: + types = [get_types(residue, atomname) for atomname in impr] + except ValueError: + continue + improper_found = False + for key in product(*types): + # Search for an improper that matches these types + improper = self.match_improper_type(*key) + if improper is None: + continue + # Add this to our types + if isinstance(improper, ImproperType): + improper_harmonic[key] = improper + improper_found = True + elif isinstance(improper, DihedralType): + improper_periodic[key] = improper + improper_found = True + else: + raise RuntimeError( + f'Something went wrong with improper type for {key} returning an unexpected object {improper}' + ) + + # Warn if no improper was found + if not improper_found: + raise RuntimeError(f'No improper found for improper {impr} in residue {name} (types were {types})') + + # Update our impropers + self.improper_periodic_types = improper_periodic + self.improper_types = improper_harmonic + + def _compress_impropers(self): + """ + OpenMM's ForceField cannot handle impropers that match the same four atoms in more than + one order, so we should compress duplicates and aggregate spring constants accordingly. + """ + if not self.improper_types: + return + + unique_keys = OrderedDict() # unique_keys[key] is the key to retrieve the improper from improper_types + improper_types = OrderedDict() # replacement for self.improper_types with compressed impropers + for atoms, improper in self.improper_types.items(): + # Compute a unique key + unique_key = tuple(sorted(atoms)) + if unique_key in unique_keys: + # Accumulate spring constant, discarding this contribution + atoms2 = unique_keys[unique_key] + if improper.psi_eq != improper_types[atoms2].psi_eq: + warnings.warn( + f"Two impropers for atom types {atoms2} found with different equilibrium " + f"angles: {improper.psi_eq} != {improper_types[atoms2].psi_eq}", + ParameterWarning, + ) + improper_types[atoms2].psi_k += improper.psi_k + warnings.warn(f'Compressing improper {improper} because it contains same atoms as {improper_types[atoms2]}') + else: + # Store this improper + unique_keys[unique_key] = atoms + improper_types[atoms] = improper + + self.improper_types = improper_types + + def _find_unused_residues(self): + skip_residues = set() + for name, residue in self.residues.items(): + if any((atom.type not in self.atom_types for atom in residue.atoms)): + skip_residues.add(name) + return skip_residues + + def _find_unused_types(self, skip_residues): + keep_types = set() + for name, residue in self.residues.items(): + if name not in skip_residues: + for atom in residue.atoms: + keep_types.add(atom.type) + return {typ for typ in self.atom_types if typ not in keep_types} + + @staticmethod + def _templhasher(residue): + """ + Create a unique hash for each residue and patch template using only properties rendered to OpenMM ffxml. + """ + hash_info = tuple() + # Sort tuples of atom properties by atom name + if len(residue.atoms) > 0: + hash_info += tuple(sorted( [(atom.type, str(atom.charge)) for atom in residue.atoms] )) + # Sort list of deleted atoms by atom name + if hasattr(residue, 'delete_atoms') and len(residue.delete_atoms) > 0: + hash_info += tuple(sorted([atom_name for atom_name in residue.delete_atoms])) + # Sort list of bonds by first bond name + if len(residue.bonds) > 0: + hash_info += tuple(sorted([(bond.atom1.name, bond.atom2.name) if (bond.atom1.name < bond.atom2.name) else (bond.atom2.name, bond.atom1.name) for bond in residue.bonds] )) + # Add head and tail + if residue.head: + hash_info += (residue.head.name,) + if residue.tail: + hash_info += (residue.tail.name,) + # TODO: Is there any other data that is rendered to ffxml files we should include? + return hash(hash_info) + + def _write_omm_provenance(self, root, provenance): + info = etree.SubElement(root, 'Info') + + date_generated = etree.SubElement(info, "DateGenerated") + ttup = datetime.datetime.now().timetuple() + date_generated.text = f'{ttup[0]:02d}-{ttup[1]:02d}-{ttup[2]:02d}' + + provenance = provenance or OrderedDict() + for tag, content in provenance.items(): + if tag == 'DateGenerated': + continue + if not isinstance(content, list): + content = [content] + for sub_content in content: + if isinstance(sub_content, str): + item = etree.Element(tag) + item.text = sub_content + info.append(item) + elif isinstance(sub_content, Mapping): + if tag not in sub_content: + raise KeyError('Content of an attribute-containing element specified incorrectly.') + attributes = [key for key in sub_content if key != tag] + element_content = sub_content[tag] + attributes = { k : str(v) for (k,v) in sub_content.items() } + item = etree.SubElement(info, tag, **attributes) + item.text = str(element_content) + else: + raise TypeError(f'Incorrect type of the {tag} element content') + + def _write_omm_atom_types(self, xml_root, skip_types, skip_residues): + if not self.atom_types: return + xml_section = etree.SubElement(xml_root, "AtomTypes") + if self.unique_atom_types: + for residue in list(self.residues.values())+list(self.patches.values()): + if residue.name in skip_residues: continue + for atom in residue.atoms: + atom_type = self.atom_types[atom.type] + properties = { 'name' : self._get_mm_atom_type(atom, residue), 'class' : atom.type, 'mass' : str(atom_type.mass) } + if atom_type.atomic_number != 0: + properties['element'] = str(Element[atom_type.atomic_number]) + etree.SubElement(xml_section, 'Type', **properties) + if isinstance(atom, DrudeAtom): + properties = { 'name' : self._get_mm_atom_type(atom, residue, True), 'class' : atom.drude_type, 'mass' : '0.0' } + etree.SubElement(xml_section, 'Type', **properties) + else: + for name, atom_type in self.atom_types.items(): + if name in skip_types: continue + assert atom_type.atomic_number >= 0, 'Atomic number not set!' + properties = { 'name' : name, 'class' : name, 'mass' : str(atom_type.mass) } + if atom_type.atomic_number == 0: + etree.SubElement(xml_section, 'Type', **properties) + else: + element = Element[atom_type.atomic_number] + etree.SubElement(xml_section, 'Type', element=str(element), **properties) + + def _get_lonepair_parameters(self, lonepair): + (lptype, a1, a2, a3, a4, r, theta, phi) = lonepair + if lptype == 'relative': + xweights = [-1.0, 0.0, 1.0] + elif lptype == 'bisector': + xweights = [-1.0, 0.5, 0.5] + else: + raise ValueError('Unknown lonepair type: '+lptype) + r /= 10.0 # convert to nanometers + theta *= math.pi / 180.0 # convert to radians + phi = (180 - phi) * math.pi / 180.0 # convert to radians + p = [r*math.cos(theta), r*math.sin(theta)*math.cos(phi), r*math.sin(theta)*math.sin(phi)] + p = [x if abs(x) > 1e-10 else 0 for x in p] # Avoid tiny numbers caused by roundoff error + return dict(type="localCoords", + siteName=a1, atomName1=a2, atomName2=a3, atomName3=a4, + wo1="1", wo2="0", wo3="0", + wx1=str(xweights[0]), wx2=str(xweights[1]), wx3=str(xweights[2]), + wy1="0", wy2="-1", wy3="1", + p1=str(p[0]), p2=str(p[1]), p3=str(p[2])) + + def _get_atoms_with_external_bonds(self, residue_name): + """ + Given a (glycan) residue, generate a list of atoms in the residue with external bonds + + Parameters + ---------- + residue_name : str + name of the residue to get external bonds for + + Returns + ------- + external bonds : list of parmed.topologyobjects.Atom + atoms in the residue with external bonds + """ + + # If its a sulfate (special case), the S is the atom with an external bond + if residue_name == 'SO3': + return [atom for atom in self.residues[residue_name] if atom.name == 'S1'] + + # Define dict of valences for each atom (in glycan residues) + d = {'C': 4, 'N': 3, 'O': 2, 'H': 1} # Key: atom element name, Value: number of bonds it should have + + # Iterate over the atoms in the residue and determine whether it has an external bond + external_bonds = [] + for atom in self.residues[residue_name].atoms: + bonds = 0 + + # Add an extra bond if the atom is the O in a carbonyl (double bond) + if atom.name in ['OD1', 'O', 'O2N', 'OXT']: + bonds += 1 + + # Count the number of bonds this atom has within the residue + for bond in self.residues[residue_name].bonds: + if bond.atom1 == atom or bond.atom2 == atom: + bonds += 1 + + # Add extra bonds if the atom is the C in a carbonyl + if (bond.atom1.name == 'OD1' and bond.atom2 == atom) or (bond.atom1 == atom and bond.atom2.name == 'OD1'): + bonds += 1 + elif (bond.atom1.name == 'O' and bond.atom2 == atom) or (bond.atom1 == atom and bond.atom2.name == 'O'): + bonds += 1 + elif (bond.atom1.name == 'O2N' and bond.atom2 == atom) or (bond.atom1 == atom and bond.atom2.name == 'O2N'): + bonds += 1 + elif (bond.atom1.name == 'OXT' and bond.atom2 == atom) or (bond.atom1 == atom and bond.atom2.name == 'OXT'): + bonds += 1 + + # Get the atom's element name + if atom.element_name == 'Og': # If the atom is in a glycan, the element names are not set properly + element_name = atom.name[0] + else: + element_name = atom.element_name + + # If the number of bonds within the residue does not equal the number of bonds the atom should have, the atom has an external bond + if d[element_name] != bonds: + external_bonds.append(atom) + + return external_bonds + + + def _write_omm_residues(self, xml_root, skip_residues, skip_duplicates, valid_patches_for_residue=None, is_glycam=False): + if not self.residues: return + if valid_patches_for_residue is None: + valid_patches_for_residue = OrderedDict() + written_residues = OrderedDict() + xml_section = etree.SubElement(xml_root, 'Residues') + for name, residue in self.residues.items(): + if name in skip_residues: continue + templhash = OpenMMParameterSet._templhasher(residue) + if templhash in written_residues: + residue_collision = written_residues[templhash] + if skip_duplicates: + warnings.warn(f'Skipping writing of residue {residue} because OpenMM considers it identical to {residue_collision}') + continue + else: + warnings.warn(f'Residue {residue} will be considered by OpenMM to be identical to {residue_collision}.') + written_residues[templhash] = residue + # Write residue + if residue.override_level == 0: + xml_residue = etree.SubElement(xml_section, 'Residue', name=residue.name) + else: + xml_residue = etree.SubElement(xml_section, 'Residue', name=residue.name, override=str(residue.override_level)) + # Write residue contents + for atom in residue.atoms: + if isinstance(atom, DrudeAtom): + etree.SubElement(xml_residue, 'Atom', name=atom.name, type=self._get_mm_atom_type(atom, residue), charge=str(atom.charge-atom.drude_charge)) + etree.SubElement(xml_residue, 'Atom', name='D'+atom.name, type=self._get_mm_atom_type(atom, residue, True), charge=str(atom.drude_charge)) + else: + etree.SubElement(xml_residue, 'Atom', name=atom.name, type=self._get_mm_atom_type(atom, residue), charge=str(atom.charge)) + for bond in residue.bonds: + etree.SubElement(xml_residue, 'Bond', atomName1=bond.atom1.name, atomName2=bond.atom2.name) + for lonepair in residue.lonepairs: + etree.SubElement(xml_residue, 'VirtualSite', self._get_lonepair_parameters(lonepair)) + for atom in residue.connections: + etree.SubElement(xml_residue, 'ExternalBond', atomName=atom.name) + if residue.head is not None: + etree.SubElement(xml_residue, 'ExternalBond', atomName=residue.head.name) + if residue.tail is not None and residue.tail is not residue.head: + etree.SubElement(xml_residue, 'ExternalBond', atomName=residue.tail.name) + if is_glycam: + external_bonds = self._get_atoms_with_external_bonds(name) + for atom in external_bonds: + if atom != residue.head and atom != residue.tail: + etree.SubElement(xml_residue, 'ExternalBond', atomName=atom.name) + if residue.name in valid_patches_for_residue: + for patch_name in valid_patches_for_residue[residue.name]: + etree.SubElement(xml_residue, 'AllowPatch', name=patch_name) + + def _determine_valid_patch_combinations(self, skip_residues): + """ + Determine valid (permissible) combinations of patches with residues that + lead to integral net charges. + + Parameters + ---------- + skip_residues : set of ResidueTemplate + List of residues to skip + + Returns + ------- + valid_residues_for_patch : dict + valid_residues_for_patch[patch] is a list of residues compatible with that patch + valid_patches_for_residue : dict + valid_patches_for_residue[residue] is a list of patches compatible with that residue + + """ + # Attempt to patch every residue, recording only valid combinations. + valid_residues_for_patch = OrderedDict() + for patch in self.patches.values(): + valid_residues_for_patch[patch.name] = list() + valid_patches_for_residue = OrderedDict() + for residue in self.residues.values(): + valid_patches_for_residue[residue.name] = list() + + # Create list of residues to check compatibility against + residues = [residue for residue in self.residues.values() if (residue not in skip_residues)] + + # Check patch compatibilities + for patch in self.patches.values(): + residue_compatibilities = [residue.patch_is_compatible(patch) for residue in residues] + for (residue, is_compatible) in zip(residues, residue_compatibilities): + if is_compatible: + valid_residues_for_patch[patch.name].append(residue.name) + valid_patches_for_residue[residue.name].append(patch.name) + + return [valid_residues_for_patch, valid_patches_for_residue] + + def _write_omm_patches(self, xml_root, valid_residues_for_patch, write_apply_to_residue=False): + """ + Write patch definitions for OpenMM ForceField + + Parameters + ---------- + xml_root : lxml.etree.Element + The XML Element write the section to. + valid_residues_for_patch : dict of str : str + valid_residues_for_patch[patch_name] lists the residue names valid for this patch + write_apply_to_residue : bool, optional, default=False + If True, will write tags. + + """ + if not self.patches: return + written_patches = OrderedDict() + xml_patches = etree.SubElement(xml_root, 'Patches') + for name, patch in self.patches.items(): + # Require that at least one valid patch combination exists for this patch + if (name not in valid_residues_for_patch) or (len(valid_residues_for_patch[name])==0): + continue + + templhash = OpenMMParameterSet._templhasher(patch) # TODO: this may be redundant now + if templhash in written_patches: + patch_collision = written_patches[templhash] + warnings.warn(f'Skipping writing of patch {patch} because OpenMM considers it identical to {patch_collision}') + continue + written_patches[templhash] = patch + if patch.override_level == 0: + patch_xml = etree.SubElement(xml_patches, 'Patch', name=patch.name) + else: + patch_xml = etree.SubElement(xml_patches, 'Patch', name=patch.name, override=str(patch.override_level)) + + # To generate the patch definition, we need to apply it to a residue and see exactly what + # changes. We might get different definitions depending on which residue we pick, so try + # all possible residues to take the most common result. + + versions = {} + for residue_name in valid_residues_for_patch[name]: + try: + residue = self.residues[residue_name] + except KeyError as err: + msg = ( + 'Compatible residue not found in self.residues\n' + f' patch name: {name}\n' + f' valid patch combinations: {valid_residues_for_patch[name]}\n' + f' residue name: {residue_name}\n' + ) + raise KeyError(msg) from err + patched_residue = residue.apply_patch(patch) + + instructions = [] + for atom in patch.atoms: + if atom.name not in residue: + command = 'AddAtom' + else: + command = 'ChangeAtom' + if isinstance(atom, DrudeAtom): + instructions.append((command, dict(name=atom.name, type=self._get_mm_atom_type(atom, patch), charge=str(atom.charge-atom.drude_charge)))) + instructions.append((command, dict(name='D'+atom.name, type=self._get_mm_atom_type(atom, patch, True), charge=str(atom.drude_charge)))) + else: + instructions.append((command, dict(name=atom.name, type=self._get_mm_atom_type(atom, patch), charge=str(atom.charge)))) + + for atom_name in patch.delete_atoms: + instructions.append(('RemoveAtom', dict(name=atom_name))) + + for bond in patch.bonds: + instructions.append(('RemoveBond', dict(atomName1=bond.atom1.name, atomName2=bond.atom2.name))) + + for bond in patched_residue.bonds: + if (bond.atom1.name not in residue) or (bond.atom2.name not in residue): + if (bond.atom1.atomic_number != 0) or (bond.atom2.atomic_number != 0): # CHARMM adds bonds to lone pairs, which we need to omit. + instructions.append(('AddBond', dict(atomName1=bond.atom1.name, atomName2=bond.atom2.name))) + for bond in residue.bonds: + if (bond.atom1.name not in patched_residue) or (bond.atom2.name not in patched_residue): + instructions.append(('RemoveBond', dict(atomName1=bond.atom1.name, atomName2=bond.atom2.name))) + + if (residue.head is not None) and (patched_residue.head is None): + instructions.append(('RemoveExternalBond', dict(atomName=residue.head.name))) + if (residue.tail is not None) and (patched_residue.tail is None): + instructions.append(('RemoveExternalBond', dict(atomName=residue.tail.name))) + + if (residue.head is None) and (patched_residue.head is not None): + instructions.append(('AddExternalBond', dict(atomName=patched_residue.head.name))) + if (residue.tail is None) and (patched_residue.tail is not None): + instructions.append(('AddExternalBond', dict(atomName=patched_residue.tail.name))) + for lonepair in patch.lonepairs: + instructions.append(('VirtualSite', self._get_lonepair_parameters(lonepair))) + + if write_apply_to_residue: + for residue_name in valid_residues_for_patch[patch.name]: + instructions.append(('ApplyToResidue', dict(name=residue_name))) + + # Convert to hashable types + instructions = tuple((i[0], tuple(item for item in i[1].items())) for i in instructions) + if instructions in versions: + versions[instructions] += 1 + else: + versions[instructions] = 1 + + # Write the consensus definition. + max_count = max(versions.values()) + instructions = [key for key, value in versions.items() if value == max_count][0] + for command, attrib in instructions: + etree.SubElement(patch_xml, command, dict(attrib)) + + def _write_omm_bonds(self, xml_root, skip_types): + if not self.bond_types: return + xml_force = etree.SubElement(xml_root, 'HarmonicBondForce') + bonds_done = set() + lconv = u.angstroms.conversion_factor_to(u.nanometers) + kconv = u.kilocalorie.conversion_factor_to(u.kilojoule) / lconv**2 * 2 + for (a1, a2), bond in self.bond_types.items(): + if any((a in skip_types for a in (a1, a2))): + continue + if (a1, a2) in bonds_done: + continue + bonds_done.add((a1, a2)) + bonds_done.add((a2, a1)) + etree.SubElement(xml_force, 'Bond', class1=a1, class2=a2, length=str(bond.req*lconv), k=str(bond.k*kconv)) + + def _write_omm_angles(self, xml_root, skip_types): + if not self.angle_types: return + xml_force = etree.SubElement(xml_root, 'HarmonicAngleForce') + angles_done = set() + tconv = u.degree.conversion_factor_to(u.radians) + kconv = u.kilocalorie.conversion_factor_to(u.kilojoule) * 2 + for (a1, a2, a3), angle in self.angle_types.items(): + if any((a in skip_types for a in (a1, a2, a3))): continue + if (a1, a2, a3) in angles_done: continue + angles_done.add((a1, a2, a3)) + angles_done.add((a3, a2, a1)) + etree.SubElement(xml_force, 'Angle', class1=a1, class2=a2, class3=a3, angle=str(angle.theteq*tconv), k=str(angle.k*kconv)) + + def _write_omm_dihedrals(self, xml_root, skip_types, improper_dihedrals_ordering): + if not self.dihedral_types and not self.improper_periodic_types: return + # In ParameterSet, dihedral_types is *always* of type DihedralTypeList. + # The from_structure method ensures that, even if the containing + # Structure has separate dihedral entries for each torsion + if improper_dihedrals_ordering == 'default': + xml_force = etree.SubElement(xml_root, 'PeriodicTorsionForce') + else: + xml_force = etree.SubElement(xml_root, 'PeriodicTorsionForce', ordering=improper_dihedrals_ordering) + diheds_done = set() + pconv = u.degree.conversion_factor_to(u.radians) + kconv = u.kilocalorie.conversion_factor_to(u.kilojoule) + def nowild(name): + return name if name != 'X' else '' + for (a1, a2, a3, a4), dihed in self.dihedral_types.items(): + if any((a in skip_types for a in (a1, a2, a3, a4))): continue + if (a1, a2, a3, a4) in diheds_done: continue + diheds_done.add((a1, a2, a3, a4)) + diheds_done.add((a4, a3, a2, a1)) + terms = OrderedDict() + for i, term in enumerate(dihed): + i += 1 + terms[f'periodicity{i}'] = str(term.per) + terms[f'phase{i}'] = str(term.phase*pconv) + terms[f'k{i}'] = str(term.phi_k*kconv) + etree.SubElement(xml_force, 'Proper', class1=nowild(a1), class2=a2, class3=a3, class4=nowild(a4), **terms) + # Now do the periodic impropers. OpenMM expects the central atom to be + # listed first. ParameterSet goes out of its way to list it third + # (consistent with Amber) except in instances where order is random (as + # in CHARMM parameter files). But CHARMM parameter files don't have + # periodic impropers, so we don't have to worry about that here. + for (a2, a3, a1, a4), improp in self.improper_periodic_types.items(): + if any((a in skip_types for a in (a1, a2, a3, a4))): continue + # Try to make the wild-cards in the middle + if a4 == 'X': + if a2 != 'X': + a2, a4 = a4, a2 + elif a3 != 'X': + a3, a4 = a4, a3 + if a2 != 'X' and a3 == 'X': + # Single wild-card entries put the wild-card in position 2 + a2, a3 = a3, a2 + etree.SubElement(xml_force, 'Improper', class1=a1, class2=nowild(a2), class3=nowild(a3), class4=nowild(a4), + periodicity1=str(improp.per), phase1=str(improp.phase*pconv), k1=str(improp.phi_k*kconv)) + + def _write_omm_impropers(self, xml_root, skip_types): + if not self.improper_types: return + xml_force = etree.SubElement(xml_root, 'CustomTorsionForce', energy="k*(theta-theta0)^2") + etree.SubElement(xml_force, 'PerTorsionParameter', name="k") + etree.SubElement(xml_force, 'PerTorsionParameter', name="theta0") + kconv = u.kilocalorie.conversion_factor_to(u.kilojoule) + tconv = u.degree.conversion_factor_to(u.radian) + def nowild(name): + return name if name != 'X' else '' + for (a1, a2, a3, a4), improp in self.improper_types.items(): + if any((a in skip_types for a in (a1, a2, a3, a4))): continue + etree.SubElement(xml_force, 'Improper', class1=nowild(a1), class2=nowild(a2), class3=nowild(a3), class4=nowild(a4), + k=str(improp.psi_k*kconv), theta0=str(improp.psi_eq*tconv)) + + def _write_omm_urey_bradley(self, xml_root, skip_types): + if not self.urey_bradley_types: return None + xml_root.append( etree.Comment("Urey-Bradley terms") ) + xml_force = etree.SubElement(xml_root, 'AmoebaUreyBradleyForce') + length_conv = u.angstroms.conversion_factor_to(u.nanometers) + _ambfrc = u.kilocalorie_per_mole/u.angstrom**2 + _ommfrc = u.kilojoule_per_mole/u.nanometer**2 + frc_conv = _ambfrc.conversion_factor_to(_ommfrc) + ureys_done = set() + for (a1, a2, a3), urey in self.urey_bradley_types.items(): + if any((a in skip_types for a in (a1, a2, a3))): continue + if (a1, a2, a3) in ureys_done: continue + if urey == NoUreyBradley: continue + etree.SubElement(xml_force, 'UreyBradley', class1=a1, class2=a2, class3=a3, d=str(urey.req*length_conv), k=str(urey.k*frc_conv)) + + def _write_omm_cmaps(self, xml_root, skip_types): + if not self.cmap_types: return + xml_force = etree.SubElement(xml_root, 'CMAPTorsionForce') + maps = OrderedDict() + counter = 0 + econv = u.kilocalorie.conversion_factor_to(u.kilojoule) + for _, cmap in self.cmap_types.items(): + if id(cmap) in maps: continue + maps[id(cmap)] = counter + counter += 1 + xml_map = etree.SubElement(xml_force, 'Map') + grid = cmap.grid.switch_range().T + map_string = '' + for i in range(cmap.resolution): + base = i * cmap.resolution + for j in range(cmap.resolution): + map_string += ' %s' % (grid[base+j]*econv) + map_string += '\n' + xml_map.text = map_string + used_torsions = set() + for (a1, a2, a3, a4, _, _, _, a5), cmap in self.cmap_types.items(): + if any((a in skip_types for a in (a1, a2, a3, a4, a5))): continue + if (a1, a2, a3, a4, a5) in used_torsions: continue + used_torsions.add((a1, a2, a3, a4, a5)) + used_torsions.add((a5, a4, a3, a2, a1)) + etree.SubElement(xml_force, 'Torsion', map=str(maps[id(cmap)]), + class1=a1, class2=a2, class3=a3, class4=a4, class5=a5) + + def _write_omm_nonbonded(self, xml_root, skip_types, separate_ljforce): + if not self.atom_types: return + # Compute conversion factors for writing in natrual OpenMM units. + length_conv = u.angstrom.conversion_factor_to(u.nanometer) + ene_conv = u.kilocalories.conversion_factor_to(u.kilojoules) + + # Get the 1-4 scaling factors from the torsion list + scee, scnb = set(), set() + unscaled_atom_types = set() + for key in self.dihedral_types: + dt = self.dihedral_types[key] + for t in dt: + if t.scee == 1 and t.scnb == 1: + unscaled_atom_types.add(key) + else: + if t.scee: scee.add(t.scee) + if t.scnb: scnb.add(t.scnb) + if len(unscaled_atom_types) > 0: + # If no 1-4 interactions are scaled, set the scale factors to 1.0. + if len(scee) == 0: + scee = {1.0} + if len(scnb) == 0: + scnb = {1.0} + if len(scee) > 1 or len(scnb) > 1: + scee_facs = ', '.join([str(x) for x in scee]) + scnb_facs = ', '.join([str(x) for x in scnb]) + raise NotImplementedError( + f'Cannot currently handle mixed 1-4 scaling: 1-4 eel [{scee_facs}] 1-4 vdw [{scnb_facs}]' + ) + coulomb14scale = 1.0 / scee.pop() if scee else 1.0 / self.default_scee + lj14scale = 1.0 / scnb.pop() if scnb else 1.0 / self.default_scnb + + # Write NonbondedForce records. + xml_force = etree.SubElement(xml_root, 'NonbondedForce', coulomb14scale=str(coulomb14scale), lj14scale=str(lj14scale)) + etree.SubElement(xml_force, 'UseAttributeFromResidue', name="charge") + for name, atom_type in self.atom_types.items(): + if name in skip_types: continue + if (atom_type.rmin is not None) and (atom_type.epsilon is not None): + sigma = atom_type.sigma * length_conv # in md_unit_system + epsilon = atom_type.epsilon * ene_conv # in md_unit_system + else: + # Dummy atom + sigma = 1.0 + epsilon = 0.0 + + if self.nbfix_types or separate_ljforce: + # turn off L-J. Will use LennardJonesForce to use CostumNonbondedForce to compute L-J interactions + sigma = 1.0 + epsilon = 0.0 + else: + # NonbondedForce cannot handle distinct 14 parameters + # We need to use a separate LennardJonesForce instead + # TODO: Can we autodetect this and switch on separate_ljforce earlier? + if (atom_type.rmin_14 != atom_type.rmin) or (atom_type.epsilon_14 != atom_type.epsilon): + raise NotImplementedError( + 'OpenMM cannot handle distinct 1-4 sigma and epsilon ' + 'parameters; use separate_ljforce=True instead' + ) + + # Ensure we don't have sigma = 0 + if (sigma == 0.0): + if (epsilon == 0.0): + sigma = 1.0 # reset sigma = 1 + else: + raise ValueError(f"For atom type '{name}', sigma = 0 but epsilon != 0.") + + attributes = { 'class' : name, 'sigma' : str(sigma), 'epsilon' : str(abs(epsilon)) } + etree.SubElement(xml_force, 'Atom', **attributes) + + if len(unscaled_atom_types) > 0 and (coulomb14scale != 1 or lj14scale != 1): + # Some 1-4 interactions should be unscaled. Add a script to fix them. + types = ',\n '.join('("%s","%s","%s","%s")' % s for s in sorted(unscaled_atom_types)) + script = etree.SubElement(xml_root, 'Script') + script.text = """ +# Some 1-4 interactions should be unscaled. + +import openmm as mm +import openmm.unit as unit +import math +unscaled_types = set([%s]) + +# Identify 1-4 pairs whose interactions should not be scaled. + +atom_types = [data.atomType[atom] for atom in data.atoms] +unscaled_pairs = set() +for p1, p2, p3, p4 in data.propers: + types = (atom_types[p1], atom_types[p2], atom_types[p3], atom_types[p4]) + if types in unscaled_types or reversed(types) in unscaled_types: + unscaled_pairs.add((p1, p4)) + +# Fix the exception parameters for those pairs. + +for force in sys.getForces(): + if isinstance(force, mm.NonbondedForce): + atom_charges = {} + atom_sigmas = {} + atom_epsilons = {} + for atom in data.atoms: + index = atom.index + charge, sigma, epsilon = force.getParticleParameters(index) + atom_charges[index] = charge + atom_sigmas[index] = sigma + atom_epsilons[index] = epsilon + for i in range(force.getNumExceptions()): + p1, p2, chargeProd, sigma, epsilon = force.getExceptionParameters(i) + if chargeProd._value != 0 or epsilon._value != 0: + if (p1, p2) in unscaled_pairs or (p2, p1) in unscaled_pairs: + force.setExceptionParameters(i, p1, p2, atom_charges[p1]*atom_charges[p2], (atom_sigmas[p1]+atom_sigmas[p2])/2, unit.sqrt(atom_epsilons[p1]*atom_epsilons[p2])) +""" % types + + def _write_omm_LennardJonesForce(self, xml_root, skip_types, separate_ljforce): + if not self.nbfix_types and not separate_ljforce: return + # Convert Conversion factors for writing in natural OpenMM units + length_conv = u.angstrom.conversion_factor_to(u.nanometer) + ene_conv = u.kilocalories.conversion_factor_to(u.kilojoules) + + scnb = set() + for key in self.dihedral_types: + dt = self.dihedral_types[key] + for t in dt: + if t.scnb: scnb.add(t.scnb) + if len(scnb) > 1: + scnb_str = ', '.join([str(x) for x in scnb]) + raise NotImplementedError( + f'Cannot currently handle mixed 1-4 scaling: L-J Scaling factors {scnb_str} detected' + ) + lj14scale = 1.0 / scnb.pop() if scnb else 1.0 / self.default_scnb + + # write L-J records + xml_force = etree.SubElement(xml_root, 'LennardJonesForce', lj14scale=str(lj14scale)) + for name, atom_type in self.atom_types.items(): + if name in skip_types: continue + if (atom_type.rmin is not None) and (atom_type.epsilon is not None): + sigma = atom_type.sigma * length_conv # in md_unit_system + epsilon = atom_type.epsilon * ene_conv # in md_unit_system + else: + # Dummy atom + sigma = 1.0 + epsilon = 0.0 + + # Ensure we don't have sigma = 0 + if (sigma == 0.0): + if (epsilon == 0.0): + sigma = 1.0 # reset sigma = 1 + else: + raise ValueError(f"For atom type '{name}', sigma = 0 but epsilon != 0.") + + # Handle special values used for 14 interactions + if (atom_type.rmin_14 != atom_type.rmin) or (atom_type.epsilon_14 != atom_type.epsilon): + sigma14 = atom_type.sigma_14 * length_conv # in md_unit_system + epsilon14 = atom_type.epsilon_14 * ene_conv # in md_unit_system + + # Ensure we don't have sigma = 0 + if sigma14 == 0.0: + if (epsilon14 == 0.0): + sigma14 = 1.0 # reset sigma = 1 + else: + raise ValueError(f"For atom type '{name}', sigma_14 = 0 but epsilon_14 != 0.") + else: + sigma14 = None + epsilon14 = None + + attributes = { 'class' : name, 'sigma' : str(sigma), 'epsilon' : str(abs(epsilon)) } + if epsilon14 is not None: + attributes['epsilon14'] = str(abs(epsilon14)) + if sigma14 is not None: + attributes['sigma14'] = str(sigma14) + etree.SubElement(xml_force, 'Atom', **attributes) + + # write NBFIX records + for (atom_types, value) in self.nbfix_types.items(): + emin = value[0] * ene_conv + rmin = value[1] * length_conv + # convert to sigma; note that NBFIX types are not rmin/2 but rmin + sigma = rmin/(2**(1.0/6)) + etree.SubElement(xml_force, 'NBFixPair', class1=atom_types[0], class2=atom_types[1], sigma=str(sigma), epsilon=str(emin)) + + def _write_omm_DrudeForce(self, xml_root, skip_types): + # Find all atoms with Drude particles. + drude_atoms = [] + for residue in list(self.residues.values())+list(self.patches.values()): + for atom in residue.atoms: + if isinstance(atom, DrudeAtom): + drude_atoms.append((atom, residue)) + if len(drude_atoms) == 0: + return + if not self.unique_atom_types: + raise ValueError('Drude particles require unique_atom_types') + xml_force = etree.SubElement(xml_root, 'DrudeForce') + alpha_scale = (1*u.angstrom/u.nanometers)**3 + for atom, residue in drude_atoms: + attributes = { + 'type1' : self._get_mm_atom_type(atom, residue, True), + 'type2' : self._get_mm_atom_type(atom, residue), + 'charge' : str(atom.drude_charge), 'polarizability' : str(abs(alpha_scale*atom.alpha)), + 'thole' : str(atom.thole), + } + if atom.anisotropy is not None: + aniso = atom.anisotropy + attributes['type3'] = self._get_mm_atom_type(aniso.atom2, residue) + attributes['type4'] = self._get_mm_atom_type(aniso.atom3, residue) + attributes['type5'] = self._get_mm_atom_type(aniso.atom4, residue) + attributes['aniso12'] = str(aniso.a11) + attributes['aniso34'] = str(aniso.a22) + etree.SubElement(xml_force, 'Particle', **attributes) + + def _write_omm_scripts(self, dest, skip_types): + # Not currently implemented, so throw an exception if any unsupported options are specified + if self.combining_rule == 'geometric': + raise NotImplementedError('Geometric combining rule not currently supported.') diff --git a/pmx/workflow/parmed/openmm/reporters.py b/pmx/workflow/parmed/openmm/reporters.py new file mode 100644 index 00000000..3ebfd7dc --- /dev/null +++ b/pmx/workflow/parmed/openmm/reporters.py @@ -0,0 +1,886 @@ +""" +This module contains a handful of extra reporter classes for OpenMM +simulations +""" +from math import isnan, isinf +from time import time + +from ..amber.asciicrd import AmberMdcrd +from ..geometry import box_vectors_to_lengths_and_angles +from ..amber.netcdffiles import NetCDFTraj +from ..amber.readparm import Rst7 +from .. import unit as u +from ..utils.decorators import needs_openmm +VELUNIT = u.angstrom / u.picosecond +FRCUNIT = u.kilocalorie_per_mole / u.angstrom + +class StateDataReporter: + """ + This class acts as a state data reporter for OpenMM simulations, but it is a + little more generalized. Notable differences are: + + - It allows the units of the output to be specified, with defaults being + those used in Amber (e.g., kcal/mol, angstroms^3, etc.) + - It will write to any object containing a 'write' method; not just + files. This allows, for instance, writing to a GUI window that + implements the desired 'write' attribute. + + Most of this code is copied from the OpenMM StateDataReporter class, with + the above-mentioned changes made. + + Parameters + ---------- + f : str or file-like + Destination to write the state data (file name or file object) + reportInterval : int + Number of steps between state data reports + step : bool, optional + Print out the step number (Default True) + time : bool, optional + Print out the simulation time (Defaults True) + potentialEnergy : bool, optional + Print out the potential energy of the structure (Default True) + kineticEnergy : bool, optional + Print out the kinetic energy of the structure (Default True) + totalEnergy : bool, optional + Print out the total energy of the system (Default True) + temperature : bool, optional + Print out the temperature of the system (Default True) + volume : bool, optional + Print out the volume of the unit cell. If the system is not periodic, + the value is meaningless (Default False) + density : bool, optional + Print out the density of the unit cell. If the system is not periodic, + the value is meaningless (Default False) + separator : str, optional + The string to separate data fields (Default ',') + systemMass : float, optional + If not None, the density will be computed from this mass, since setting + a mass to 0 is used to constrain the position of that particle. (Default + None) + energyUnit : unit, optional + The units to print energies in (default unit.kilocalories_per_mole) + timeUnit : unit, optional + The units to print time in (default unit.picoseconds) + volumeUnit : unit, optional + The units print volume in (default unit.angstroms**3) + densityUnit : unit, optional + The units to print density in (default + unit.grams/unit.item/unit.milliliter) + """ + + @needs_openmm + def __init__(self, f, reportInterval, step=True, time=True, + potentialEnergy=True, kineticEnergy=True, totalEnergy=True, + temperature=True, volume=False, density=False, separator=',', + systemMass=None, energyUnit=u.kilocalories_per_mole, + timeUnit=u.picoseconds, volumeUnit=u.angstroms**3, + densityUnit=u.grams/u.item/u.milliliter): + self._reportInterval = reportInterval + self._openedFile = not hasattr(f, 'write') + if self._openedFile: + self._out = open(f, 'w') + else: + self._out = f + self._step = step + self._time = time + self._potentialEnergy = potentialEnergy + self._kineticEnergy = kineticEnergy + self._totalEnergy = totalEnergy + self._temperature = temperature + self._volume = volume + self._density = density + self._separator = separator + self._totalMass = systemMass + self._hasInitialized = False + self._needsPositions = False + self._needsVelocities = False + self._needsForces = False + self._needEnergy = (potentialEnergy or kineticEnergy or totalEnergy or temperature) + self._energyUnit = energyUnit + self._densityUnit = densityUnit + self._timeUnit = timeUnit + self._volumeUnit = volumeUnit + + def describeNextReport(self, simulation): + """ + Get information about the next report this object will generate. + + Parameters + ---------- + simulation : :class:`app.Simulation` + The simulation to generate a report for + + Returns + ------- + nsteps, pos, vel, frc, ene : int, bool, bool, bool, bool + nsteps is the number of steps until the next report + pos, vel, frc, and ene are flags indicating whether positions, + velocities, forces, and/or energies are needed from the Context + """ + steps_left = simulation.currentStep % self._reportInterval + steps = self._reportInterval - steps_left + return (steps, self._needsPositions, self._needsVelocities, self._needsForces, self._needEnergy) + + def report(self, simulation, state): + """Generate a report. + + Parameters + ---------- + simulation : :class:`app.Simulation` + The Simulation to generate a report for + state : :class:`mm.State` + The current state of the simulation + """ + if not self._hasInitialized: + self._initializeConstants(simulation) + headers = self._constructHeaders() + header = f'"{self._separator}"'.join(headers) + self._out.write(f'#"{header}"\n') + self._hasInitialized = True + + # Check for errors. + self._checkForErrors(simulation, state) + + # Query for the values + values = self._constructReportValues(simulation, state) + + # Write the values. + self._out.write(self._separator.join(str(v) for v in values) + '\n') + if hasattr(self._out, 'flush'): + self._out.flush() + + def _constructReportValues(self, simulation, state): + """ + Query the simulation for the current state of our observables of + interest. + + Parameters + ---------- + simulation : Simulation + The Simulation object to generate a report for + state : State + The current state of the simulation object + + Returns: A list of values summarizing the current state of + the simulation, to be printed or saved. Each element in the list + corresponds to one of the columns in the resulting CSV file. + """ + values = [] + if self._volume or self._density: + volume = state.getPeriodicBoxVolume() + if self._density: + density = self._totalMass / volume + ke = state.getKineticEnergy() + pe = state.getPotentialEnergy() + if self._temperature: + temp = 2 * ke / (self._dof * u.MOLAR_GAS_CONSTANT_R) + time = state.getTime() + if self._step: + values.append(simulation.currentStep) + if self._time: + values.append(time.value_in_unit(self._timeUnit)) + if self._potentialEnergy: + values.append(pe.value_in_unit(self._energyUnit)) + if self._kineticEnergy: + values.append(ke.value_in_unit(self._energyUnit)) + if self._totalEnergy: + values.append((pe + ke).value_in_unit(self._energyUnit)) + if self._temperature: + values.append(temp.value_in_unit(u.kelvin)) + if self._volume: + values.append(volume.value_in_unit(self._volumeUnit)) + if self._density: + values.append(density.value_in_unit(self._densityUnit)) + return values + + def _initializeConstants(self, simulation): + """ + Initialize a set of constants required for the reports + + Parameters + ---------- + simulation : Simulation + The simulation to generate a report for + """ + import openmm as mm + system = simulation.system + frclist = system.getForces() + if self._temperature: + # Compute the number of degrees of freedom. + dof = 0 + for i in range(system.getNumParticles()): + if system.getParticleMass(i) > 0*u.dalton: + dof += 3 + dof -= system.getNumConstraints() + if any(isinstance(frc, mm.CMMotionRemover) for frc in frclist): + dof -= 3 + self._dof = dof + if self._density: + if self._totalMass is None: + # Compute the total system mass. + self._totalMass = 0*u.dalton + for i in range(system.getNumParticles()): + self._totalMass += system.getParticleMass(i) + elif not u.is_quantity(self._totalMass): + self._totalMass = self._totalMass*u.dalton + + def _constructHeaders(self): + """Construct the headers for the CSV output + + Returns: a list of strings giving the title of each observable being + reported on. + """ + headers = [] + if self._step: + headers.append('Step') + if self._time: + headers.append('Time (ps)') + if self._potentialEnergy: + headers.append(f'Potential Energy ({self._energyUnit})') + if self._kineticEnergy: + headers.append(f'Kinetic Energy ({self._energyUnit})') + if self._totalEnergy: + headers.append(f'Total Energy ({self._energyUnit})') + if self._temperature: + headers.append('Temperature (K)') + if self._volume: + headers.append(f'Box Volume ({self._volumeUnit})') + if self._density: + headers.append(f'Density ({self._densityUnit})') + return headers + + def _checkForErrors(self, simulation, state): + """Check for errors in the current state of the simulation + + Parameters + ---------- + simulation : :class:`app.Simulation` + The Simulation to generate a report for + state : :class:`State` + The current state of the simulation + """ + if self._needEnergy: + energy = state.getKineticEnergy() + state.getPotentialEnergy() + if isnan(energy._value): + raise ValueError('Energy is NaN') # pragma: no cover + if isinf(energy._value): + raise ValueError('Energy is infinite') # pragma: no cover + + def __del__(self): + if hasattr(self, '_openedFile') and self._openedFile: + self._out.close() + + def finalize(self): + """ Closes any open file """ + try: + if self._out is not None and self._openedFile: + self._out.close() + except AttributeError: # pragma: no cover + pass # pragma: no cover + +class NetCDFReporter(object): + """ NetCDFReporter prints a trajectory in NetCDF format """ + + @needs_openmm + def __init__(self, file, reportInterval, crds=True, vels=False, frcs=False): + """ + Create a NetCDFReporter instance. + + Parameters + ---------- + file : str + Name of the file to write the trajectory to + reportInterval : int + How frequently to write a frame to the trajectory + crds : bool=True + Should we write coordinates to this trajectory? (Default True) + vels : bool=False + Should we write velocities to this trajectory? (Default False) + frcs : bool=False + Should we write forces to this trajectory? (Default False) + """ + if not crds and not vels and not frcs: + raise ValueError('Either coordinates, velocities, or forces needed for NetCDFReporter') + # Control flags + self.crds, self.vels, self.frcs = crds, vels, frcs + self._reportInterval = reportInterval + self._out = None # not written yet + self.fname = file + + def describeNextReport(self, simulation): + """ + Get information about the next report this object will generate. + + Parameters + ---------- + simulation : :class:`app.Simulation` + The Simulation to generate a report for + + Returns + ------- + nsteps, pos, vel, frc, ene : int, bool, bool, bool, bool + nsteps is the number of steps until the next report + pos, vel, frc, and ene are flags indicating whether positions, + velocities, forces, and/or energies are needed from the Context + """ + stepsleft = simulation.currentStep % self._reportInterval + steps = self._reportInterval - stepsleft + return (steps, self.crds, self.vels, self.frcs, False) + + + def report(self, simulation, state): + """Generate a report. + + Parameters + ---------- + simulation : :class:`app.Simulation` + The Simulation to generate a report for + state : :class:`mm.State` + The current state of the simulation + """ + global VELUNIT, FRCUNIT + if self.crds: + crds = state.getPositions().value_in_unit(u.angstrom) + if self.vels: + vels = state.getVelocities().value_in_unit(VELUNIT) + if self.frcs: + frcs = state.getForces().value_in_unit(FRCUNIT) + if self._out is None: + # This must be the first frame, so set up the trajectory now + if self.crds: + atom = len(crds) + elif self.vels: + atom = len(vels) + elif self.frcs: + atom = len(frcs) + self.uses_pbc = simulation.topology.getUnitCellDimensions() is not None + self._out = NetCDFTraj.open_new( + self.fname, atom, self.uses_pbc, self.crds, self.vels, + self.frcs, title="ParmEd-created trajectory using OpenMM", + ) + if self.uses_pbc: + vecs = state.getPeriodicBoxVectors() + lengths, angles = box_vectors_to_lengths_and_angles(*vecs) + self._out.add_cell_lengths_angles( + lengths.value_in_unit(u.angstrom), angles.value_in_unit(u.degree) + ) + + # Add the coordinates, velocities, and/or forces as needed + if self.crds: + self._out.add_coordinates(crds) + if self.vels: + # The velocities get scaled right before writing + self._out.add_velocities(vels) + if self.frcs: + self._out.add_forces(frcs) + # Now it's time to add the time. + self._out.add_time(state.getTime().value_in_unit(u.picosecond)) + + def __del__(self): + try: + if self._out is not None: + self._out.close() + except AttributeError: + pass + + def finalize(self): + """ Closes any open file """ + try: + if self._out is not None: + self._out.close() + except AttributeError: # pragma: no cover + pass # pragma: no cover + +class MdcrdReporter(object): + """ + MdcrdReporter prints a trajectory in ASCII Amber format. This reporter will + be significantly slower than binary file reporters (like DCDReporter or + NetCDFReporter). + + Parameters + ---------- + file : str + Name of the file to write the trajectory to + reportInterval : int + Number of steps between writing trajectory frames + crds : bool=True + Write coordinates to this trajectory file? + vels : bool=False + Write velocities to this trajectory file? + frcs : bool=False + Write forces to this trajectory file? + + Notes + ----- + You can only write one of coordinates, forces, or velocities to a mdcrd + file. + """ + + @needs_openmm + def __init__(self, file, reportInterval, crds=True, vels=False, frcs=False): + # ASCII mdcrds can have either coordinates, forces, or velocities + ntrue = 0 + if crds: ntrue += 1 + if vels: ntrue += 1 + if frcs: ntrue += 1 + if ntrue != 1: + raise ValueError( + 'MdcrdReporter must print exactly one of either coordinates, velocities, or forces.' + ) + # Control flags + self.crds, self.vels, self.frcs = crds, vels, frcs + self._reportInterval = reportInterval + self._out = None # not written yet + self.fname = file + + def describeNextReport(self, simulation): + """ + Get information about the next report this object will generate. + + Parameters + ---------- + simulation : :class:`app.Simulation` + The Simulation to generate a report for + + Returns + ------- + nsteps, pos, vel, frc, ene : int, bool, bool, bool, bool + nsteps is the number of steps until the next report + pos, vel, frc, and ene are flags indicating whether positions, + velocities, forces, and/or energies are needed from the Context + """ + stepsleft = simulation.currentStep % self._reportInterval + steps = self._reportInterval - stepsleft + return (steps, self.crds, self.vels, self.frcs, False) + + def report(self, simulation, state): + """ + Generate a report. + + Parameters: + - simulation (Simulation) The Simulation to generate a report for + - state (State) The current state of the simulation + """ + from ..amber.asciicrd import VELSCALE + global VELUNIT, FRCUNIT + if self.crds: + crds = state.getPositions().value_in_unit(u.angstrom) + elif self.vels: # crds/vels/frcs are exclusive, elif works + vels = state.getVelocities().value_in_unit(VELUNIT) + elif self.frcs: + frcs = state.getForces().value_in_unit(FRCUNIT) + if self._out is None: + # This must be the first frame, so set up the trajectory now + if self.crds: + self.atom = len(crds) + elif self.vels: + self.atom = len(vels) + elif self.frcs: + self.atom = len(frcs) + self.uses_pbc = simulation.topology.getUnitCellDimensions() is not None + self._out = AmberMdcrd(self.fname, self.atom, self.uses_pbc, + title="ParmEd-created trajectory using OpenMM", mode='w') + + # Add the coordinates, velocities, and/or forces as needed + if self.crds: + flatcrd = [0 for i in range(self.atom*3)] + for i in range(self.atom): + i3 = i*3 + flatcrd[i3], flatcrd[i3+1], flatcrd[i3+2] = crds[i] + self._out.add_coordinates(flatcrd) + if self.vels: + # Divide by the scaling factor (works if vels is a list of Vec3's) + # This is necessary since AmberMdcrd does not scale before writing + # (since it expects coordinates) + vels = [v / VELSCALE for v in vels] + flatvel = [0 for i in range(self.atom*3)] + for i in range(self.atom): + i3 = i*3 + flatvel[i3], flatvel[i3+1], flatvel[i3+2] = vels[i] + self._out.add_coordinates(flatvel) + if self.frcs: + flatfrc = [0 for i in range(self.atom*3)] + for i in range(self.atom): + i3 = i*3 + flatfrc[i3], flatfrc[i3+1], flatfrc[i3+2] = frcs[i] + self._out.add_coordinates(flatfrc) + # Now it's time to add the box lengths + if self.uses_pbc: + boxvecs = state.getPeriodicBoxVectors() + lengths, angles = box_vectors_to_lengths_and_angles(*boxvecs) + self._out.add_box(lengths.value_in_unit(u.angstroms)) + + def __del__(self): + try: + if self._out is not None: + self._out.close() + except AttributeError: + pass + + def finalize(self): + """ Closes any open file """ + try: + if self._out is not None: + self._out.close() + except AttributeError: # pragma: no cover + pass # pragma: no cover + +class RestartReporter(object): + """ + Use a reporter to handle writing restarts at specified intervals. + + Parameters + ---------- + file : str + Name of the file to write the restart to. + reportInterval : int + Number of steps between writing restart files + write_multiple : bool=False + Either write a separate restart each time (appending the step number in + the format .# to the file name given above) if True, or overwrite the + same file each time if False + netcdf : bool=False + Use the Amber NetCDF restart file format + write_velocities : bool=True + Write velocities to the restart file. You can turn this off for passing + in, for instance, a minimized structure. + """ + + @needs_openmm + def __init__(self, file, reportInterval, write_multiple=False, netcdf=False, + write_velocities=True): + self.fname = file + self._reportInterval = reportInterval + self.write_multiple = write_multiple + self.netcdf = netcdf + self.write_velocities = write_velocities + self.rst7 = None + + def describeNextReport(self, simulation): + """ + Get information about the next report this object will generate. + + Parameters + ---------- + simulation : :class:`app.Simulation` + The Simulation to generate a report for + + Returns + ------- + nsteps, pos, vel, frc, ene : int, bool, bool, bool, bool + nsteps is the number of steps until the next report + pos, vel, frc, and ene are flags indicating whether positions, + velocities, forces, and/or energies are needed from the Context + """ + stepsleft = simulation.currentStep % self._reportInterval + steps = self._reportInterval - stepsleft + return (steps, True, True, False, False) + + def report(self, sim, state): + """Generate a report. + + Parameters + ---------- + sim : :class:`app.Simulation` + The Simulation to generate a report for + state : :class:`mm.State` + The current state of the simulation + """ + global VELUNIT + crds = state.getPositions().value_in_unit(u.angstrom) + if self.rst7 is None: + self.uses_pbc = sim.topology.getUnitCellDimensions() is not None + self.atom = len(crds) + # First time written + self.rst7 = Rst7(natom=self.atom, title='Restart file written by ParmEd with OpenMM') + self.rst7.time = state.getTime().value_in_unit(u.picosecond) + flatcrd = [0.0 for i in range(self.atom*3)] + for i in range(self.atom): + i3 = i*3 + flatcrd[i3], flatcrd[i3+1], flatcrd[i3+2] = crds[i] + self.rst7.coordinates = flatcrd + + if self.write_velocities: + vels = state.getVelocities().value_in_unit(VELUNIT) + flatvel = [0.0 for i in range(self.atom*3)] + for i in range(self.atom): + i3 = i*3 + flatvel[i3], flatvel[i3+1], flatvel[i3+2] = vels[i] + self.rst7.vels = flatvel + + if self.uses_pbc: + boxvecs = state.getPeriodicBoxVectors() + lengths, angles = box_vectors_to_lengths_and_angles(*boxvecs) + lengths = lengths.value_in_unit(u.angstrom) + angles = angles.value_in_unit(u.degree) + self.rst7.box = [lengths[0], lengths[1], lengths[2], angles[0], angles[1], angles[2]] + + if self.write_multiple: + fname = self.fname + '.%d' % sim.currentStep + else: + fname = self.fname + + self.rst7.write(fname, self.netcdf) + + def finalize(self): + """ No-op here """ + pass + +class ProgressReporter(StateDataReporter): + """ + A class that prints out a progress report of how much MD (or minimization) + has been done, how fast the simulation is running, and how much time is left + (similar to the mdinfo file in Amber) + + Parameters + ---------- + f : str + The file name of the progress report file (overwritten each time) + reportInterval : int + The step interval between which to write frames + totalSteps : int + The total number of steps that will be run in the simulation (used to + estimate time remaining) + potentialEnergy : bool, optional + Whether to print the potential energy (default True) + kineticEnergy : bool, optional + Whether to print the kinetic energy (default True) + totalEnergy : bool, optional + Whether to print the total energy (default True) + temperature : bool, optional + Whether to print the system temperature (default True) + volume : bool, optional + Whether to print the system volume (default False) + density : bool, optional + Whether to print the system density (default False) + systemMass : float or :class:`unit.Quantity` + The mass of the system used when reporting density (useful in instances + where masses are set to 0 to constrain their positions) + + Notes + ----- + In addition to the above, :class:`ProgressReporter` also accepts arguments for + :class:`StateDataReporter` + """ + + @needs_openmm + def __init__(self, f, reportInterval, totalSteps, potentialEnergy=True, + kineticEnergy=True, totalEnergy=True, temperature=False, + volume=False, density=False, systemMass=None, **kwargs): + # Make sure we got a file name rather than a file-like object. + # Immediately close the file after opening. This erases it, which isn't + # a bad thing, but also prepares it for reports + kwargs['time'] = kwargs['step'] = True + super(ProgressReporter, self).__init__( + f, reportInterval, potentialEnergy=potentialEnergy, + kineticEnergy=kineticEnergy, totalEnergy=totalEnergy, + temperature=temperature, volume=volume, density=density, + systemMass=systemMass, **kwargs + ) + if not self._openedFile: + raise ValueError('ProgressReporter requires a file name (not file object)') + self._out.close() + del self._out + self.fname = f + self._totalSteps = totalSteps + + # For timing + self._startTime = None + self._firstStep = None + self._lastReportTime = None + self._timeStep = None + + def describeNextReport(self, simulation): + """ + Get information about the next report this object will generate. + + Parameters + ---------- + simulation : :class:`app.Simulation` + The Simulation to generate a report for + + Returns + ------- + nsteps, pos, vel, frc, ene : int, bool, bool, bool, bool + nsteps is the number of steps until the next report + pos, vel, frc, and ene are flags indicating whether positions, + velocities, forces, and/or energies are needed from the Context + """ + if self._startTime is None: + # First time this is called, initialize the timers + self._startTime = time() + self._lastReportTime = time() + self._timeStep = simulation.integrator.getStepSize() + self._timeStep = self._timeStep.value_in_unit(u.nanosecond) + self._firstStep = simulation.currentStep + stepsleft = simulation.currentStep % self._reportInterval + steps = self._reportInterval - stepsleft + return (steps, False, False, False, self._needEnergy) + + def report(self, simulation, state): + """ + Generate a report and predict the time to completion (and + current/overall MD performance) + """ + if not self._hasInitialized: + self._initializeConstants(simulation) + self._hasInitialized = True + + # Check for errors. + self._checkForErrors(simulation, state) + + # Query for the values + values = self._constructReportValues(simulation, state) + + now = time() + + total_time = now - self._startTime + partial_time = now - self._lastReportTime + self._lastReportTime = now + + total_nsperday = (values['step'] - self._firstStep) * self._timeStep / total_time + partial_nsperday = (self._reportInterval*self._timeStep) / partial_time + + # Get total and partial ns/day (currently ns/second) + total_nsperday *= 3600 * 24 + partial_nsperday *= 3600 * 24 + + # Estimated time to completion (based on performance of last N steps + remaining_steps = self._totalSteps - values['step'] + self._firstStep + etc = partial_time / self._reportInterval * remaining_steps + + etc, unitstr = _format_time(etc) + + # Write the values. + with open(self.fname, 'w') as f: + f.write('-+' * 39 + '\n') + f.write('\n') + f.write(f' On step {values["step"] - self._firstStep} of {self._totalSteps}\n') + f.write('\n') + if self._totalEnergy: + f.write(f' Total Energy = {values["totalEnergy"]:12.4f}\n') + if self._potentialEnergy: + f.write(f' Potential Energy = {values["potentialEnergy"]:12.4f}\n') + if self._kineticEnergy: + f.write(f' Kinetic Energy = {values["kineticEnergy"]:12.4f}\n') + if self._volume: + f.write(f' Volume = {values["volume"]:12.4f}\n') + if self._density: + f.write(f' Density = {values["density"]:12.4f}\n') + if self._temperature: + f.write(f' Temperature = {values["temperature"]:12.4f}\n') + f.write('\n') + f.write( + f' Time for last {self._reportInterval:8d} steps: {partial_time:10.4f} s. ({partial_nsperday:.3f} ns/day)\n' + ) + f.write(f' Time for all {values["step"] - self._firstStep:9d} steps: {total_time:10.4f} s. ({total_nsperday:.3f} ns/day)\n') + f.write('\n') + f.write(f' Estimated time to completion: {etc:.3f} {unitstr}\n') + f.write('\n') + f.write('-+' * 39 + '\n') + + if remaining_steps == 0: + self._startTime = None + + def _constructReportValues(self, simulation, state): + """ + Query the simulation for the current state of our observables of + interest. + + Parameters: + - simulation (Simulation) The Simulation to generate a report for + - state (State) The current state of the simulation + + Returns: A list of values summarizing the current state of the + simulation, to be printed or saved. Each element in the list + corresponds to one of the columns in the resulting CSV file. + """ + values = dict() + values['step'] = simulation.currentStep + values['time'] = state.getTime() + volume = state.getPeriodicBoxVolume() + pe = state.getPotentialEnergy().value_in_unit(self._energyUnit) + ke = state.getKineticEnergy() + if self._temperature: + temp = 2 * ke / (self._dof * u.MOLAR_GAS_CONSTANT_R) + ke = ke.value_in_unit(self._energyUnit) + if self._potentialEnergy: + values['potentialEnergy'] = pe + if self._kineticEnergy: + values['kineticEnergy'] = ke + if self._totalEnergy: + values['totalEnergy'] = pe + ke + if self._temperature: + values['temperature'] = temp.value_in_unit(u.kelvin) + if self._volume: + values['volume'] = volume.value_in_unit(self._volumeUnit) + if self._density: + dens = self._totalMass / volume + # md_unit_system does the right thing that every unit system would + # want + values['density'] = dens.value_in_unit(self._densityUnit) + + return values + + def __del__(self): + """ We already closed the file. """ + +class EnergyMinimizerReporter(StateDataReporter): + """ + This class acts as a simple energy reporter class for minimizations. This + is not meant to be used as a reporter for OpenMM's molecular dynamics + routines, but instead passed a simulation object for printing out + single-point energies. + + Parameters + ---------- + f : str or file-like + File name or object to write energies to + volume : bool, optional + If True, write the system volume (default is False) + """ + + def __init__(self, f, volume=False, **kwargs): + super().__init__(f, 1, **kwargs) + self._volume = volume + + def describeNextReport(self, *args, **kwargs): + """ Disable this reporter inside MD """ + raise NotImplementedError('EnergyMinimizerReporter is not intended for ' + 'use in reporting on molecular dynamics') + + def report(self, simulation, frame=None): + """ Print out the current energy """ + has_pbc = simulation.topology.getUnitCellDimensions() is not None + state = simulation.context.getState(getEnergy=True, enforcePeriodicBox=has_pbc) + if frame is not None: + self._out.write(f'Frame: {frame:10d}\n') + e = state.getPotentialEnergy().value_in_unit(self._energyUnit) + self._out.write(f' Potential Energy = {e:12.4f} {self._energyUnit}\n') + if has_pbc and (self._volume or self._density): + vol = state.getPeriodicBoxVolume().value_in_unit(self._volumeUnit) + if has_pbc and self._volume: + self._out.write(f' Volume = {vol:12.4f} {self._volumeUnit}\n') + self._out.write('\n') + + def finalize(self): + """ Closes any open file """ + try: + if self._out is not None: + self._out.close() + except AttributeError: # pragma: no cover + pass # pragma: no cover + +# Private helper functions +def _format_time(etc): + """ Formats how time is printed in ProgressReporter """ + if etc > 3600: + etc /= 3600 + unitstr = 'hr.' + elif etc > 60: + etc /= 60 + unitstr = 'min.' + else: + unitstr = 'sec.' + return etc, unitstr diff --git a/pmx/workflow/parmed/openmm/topsystem.py b/pmx/workflow/parmed/openmm/topsystem.py new file mode 100644 index 00000000..57b5623f --- /dev/null +++ b/pmx/workflow/parmed/openmm/topsystem.py @@ -0,0 +1,435 @@ +""" +Convert an OpenMM Topology into a Structure instance, optionally filling in +parameters from a System +""" +import warnings +from collections import defaultdict + +import numpy as np + +from .. import unit as u +from ..exceptions import OpenMMWarning +from ..formats import load_file +from ..geometry import box_vectors_to_lengths_and_angles +from ..periodic_table import Element +from ..structure import Structure +from ..topologyobjects import ( + Angle, AngleType, Atom, AtomType, Bond, BondType, Cmap, CmapType, Dihedral, DihedralType, + ExtraPoint, Improper, ImproperType, NonbondedException, NonbondedExceptionType, RBTorsionType, + UreyBradley +) +from ..utils.decorators import needs_openmm + +__all__ = ['load_topology'] + + +@needs_openmm +def load_topology(topology, system=None, xyz=None, box=None, condense_atom_types=True): + """ + Creates a :class:`parmed.structure.Structure` instance from an OpenMM + Topology, optionally filling in parameters from a System + + Parameters + ---------- + topology : :class:`openmm.app.Topology` + The Topology instance with the list of atoms and bonds for this system + system : :class:`openmm.System` or str, optional + If provided, parameters from this System will be applied to the + Structure. If a string is given, it will be interpreted as the file name + of an XML-serialized System, and it will be deserialized into a System + before used to supply parameters + xyz : str or array of float + Name of a file containing coordinate information or an array of + coordinates. If file has unit cell information, it also uses that + information unless ``box`` (below) is also specified + box : array of 6 floats + Unit cell dimensions + condense_atom_types : bool, default=True + If True, create unique atom types based on de-duplicating properties. If False, + create one atom type for each atom in the system, even if its properties match + an existing atom type. + + Returns + ------- + struct : :class:`Structure ` + The structure from the provided topology + + Raises + ------ + OpenMMWarning if parameters are found that cannot be interpreted or + processed by ParmEd + + TypeError if there are any mismatches between the provided topology and + system (e.g., they have different numbers of atoms) + + IOError if system is a string and it is not an existing file + + Notes + ----- + Due to its flexibility with CustomForces, it is entirely possible that the + functional form of the potential will be unknown to ParmEd. This function + will try to use the energy expression to identify supported potential types + that are implemented as CustomForce objects. In particular, quadratic + improper torsions, when recognized, will be extracted. + + Other CustomForces, including the CustomNonbondedForce used to implement + NBFIX (off-diagonal L-J modifications) and the 12-6-4 potential, will not be + processed and will result in an unknown functional form. + + If an OpenMM Atom.id attribute is populated by a non-integer, it will be + used to name the corresponding ParmEd AtomType object. + """ + import openmm as mm + struct = Structure() + atommap = dict() + for c in topology.chains(): + chain = c.id + for r in c.residues(): + residue = r.name + resid = r.index + for a in r.atoms(): + if a.element is None: + atom = ExtraPoint(name=a.name) + else: + try: + aid = int(a.id) + except ValueError: + aid = a.id + atype = aid if not isinstance(aid, int) else '' + atom = Atom(atomic_number=a.element.atomic_number, + name=a.name, mass=a.element.mass, type=atype) + struct.add_atom(atom, residue, resid, chain) + atommap[a] = atom + for a1, a2 in topology.bonds(): + struct.bonds.append(Bond(atommap[a1], atommap[a2])) + + vectors = topology.getPeriodicBoxVectors() + if vectors is not None: + leng, ang = box_vectors_to_lengths_and_angles(*vectors) + leng = leng.value_in_unit(u.angstroms) + ang = ang.value_in_unit(u.degrees) + struct.box = [leng[0], leng[1], leng[2], ang[0], ang[1], ang[2]] + + loaded_box = False + + if xyz is not None: + if isinstance(xyz, str): + xyz = load_file(xyz, skip_bonds=True) + struct.coordinates = xyz.coordinates + if struct.box is not None: + if xyz.box is not None: + loaded_box = True + struct.box = xyz.box + else: + struct.coordinates = xyz + + if box is not None: + loaded_box = True + struct.box = box + + if struct.box is not None: + struct.box = np.asarray(struct.box) + + if system is None: + return struct + + if isinstance(system, str): + system = load_file(system) + + if not isinstance(system, mm.System): + raise TypeError('system must be an OpenMM System object or serialized XML of an OpenMM System object') + + # We have a system, try to extract parameters from it + if len(struct.atoms) != system.getNumParticles(): + raise TypeError(f'Topology and System have different numbers of atoms ({len(struct.atoms)} vs. {system.getNumParticles()})') + + processed_forces = set() + ignored_forces = (mm.CMMotionRemover, mm.AndersenThermostat, mm.MonteCarloBarostat, + mm.MonteCarloAnisotropicBarostat, mm.MonteCarloMembraneBarostat, + mm.CustomExternalForce, mm.GBSAOBCForce, mm.CustomGBForce) + + if system.usesPeriodicBoundaryConditions(): + if not loaded_box: + vectors = system.getDefaultPeriodicBoxVectors() + leng, ang = box_vectors_to_lengths_and_angles(*vectors) + leng = leng.value_in_unit(u.angstroms) + ang = ang.value_in_unit(u.degrees) + struct.box = np.asarray([leng[0], leng[1], leng[2], ang[0], ang[1], ang[2]]) + else: + struct.box = None + + for force in system.getForces(): + if isinstance(force, mm.HarmonicBondForce): + if mm.HarmonicBondForce in processed_forces: + # Try to process this HarmonicBondForce as a Urey-Bradley term + _process_urey_bradley(struct, force) + else: + _process_bond(struct, force) + elif isinstance(force, mm.HarmonicAngleForce): + _process_angle(struct, force) + elif isinstance(force, mm.PeriodicTorsionForce): + _process_dihedral(struct, force) + elif isinstance(force, mm.RBTorsionForce): + _process_rbtorsion(struct, force) + elif isinstance(force, mm.CustomTorsionForce): + if not _process_improper(struct, force): + struct.unknown_functional = True + warnings.warn('Unknown functional form of CustomTorsionForce', OpenMMWarning) + elif isinstance(force, mm.CMAPTorsionForce): + _process_cmap(struct, force) + elif isinstance(force, mm.NonbondedForce): + _process_nonbonded(struct, force, condense_atom_types) + elif isinstance(force, ignored_forces): + continue + else: + struct.unknown_functional = True + warnings.warn(f'Unsupported Force type {force.__class__.__name__}', OpenMMWarning) + processed_forces.add(type(force)) + + return struct + +def _process_bond(struct, force): + """ Adds bond parameters to the structure """ + typemap = dict() + for ii in range(force.getNumBonds()): + i, j, req, k = force.getBondParameters(ii) + ai, aj = struct.atoms[i], struct.atoms[j] + key = (req._value, k._value) + if key in typemap: + bond_type = typemap[key] + else: + bond_type = BondType(k*0.5, req) + typemap[key] = bond_type + struct.bond_types.append(bond_type) + if aj in ai.bond_partners: + for bond in ai.bonds: + if aj in bond: + break + else: + raise RuntimeError('aj in ai.bond_partners, but couldn\'t find that bond!') + bond.type = bond_type + else: + struct.bonds.append(Bond(ai, aj, type=bond_type)) + struct.bond_types.claim() + +def _process_angle(struct, force): + """ Adds angle parameters to the structure """ + typemap = dict() + for ii in range(force.getNumAngles()): + i, j, k, theteq, frc_k = force.getAngleParameters(ii) + key = (theteq._value, frc_k._value) + ai, aj, ak = struct.atoms[i], struct.atoms[j], struct.atoms[k] + if key in typemap: + angle_type = typemap[key] + else: + angle_type = AngleType(frc_k*0.5, theteq) + typemap[key] = angle_type + struct.angle_types.append(angle_type) + struct.angles.append(Angle(ai, aj, ak, type=angle_type)) + struct.angle_types.claim() + +def _process_urey_bradley(struct, force): + """ Adds Urey-Bradley parameters to the structure """ + if not struct.angles: + warnings.warn('Adding what seems to be Urey-Bradley terms before ' + 'Angles. This is unexpected, but the parameters will ' + 'all be present in one form or another.', OpenMMWarning) + typemap = dict() + for ii in range(force.getNumBonds()): + i, j, req, k = force.getBondParameters(ii) + ai, aj = struct.atoms[i], struct.atoms[j] + key = (req._value, k._value) + if struct.angles and ai not in aj.angle_partners: + warnings.warn(f'Adding what seems to be Urey-Bradley terms, but atoms {a.idx} and ' + f'{aj.idx} do not appear to be angled to each other. Parameters will ' + 'all be present, but may not be in expected places.') + if key in typemap: + urey_type = typemap[key] + else: + urey_type = BondType(k * 0.5, req) + typemap[key] = urey_type + struct.urey_bradley_types.append(urey_type) + struct.urey_bradleys.append(UreyBradley(ai, aj, type=urey_type)) + struct.urey_bradley_types.claim() + +def _process_dihedral(struct, force): + """ Adds periodic torsions to the structure """ + typemap = dict() + for ii in range(force.getNumTorsions()): + i, j, k, l, per, phase, phi_k = force.getTorsionParameters(ii) + ai, aj = struct.atoms[i], struct.atoms[j] + ak, al = struct.atoms[k], struct.atoms[l] + key = (per, phase._value, phi_k._value) + if key in typemap: + dihed_type = typemap[key] + else: + dihed_type = DihedralType(phi_k, per, phase) + typemap[key] = dihed_type + struct.dihedral_types.append(dihed_type) + improper = ai in ak.bond_partners and aj in ak.bond_partners and al in ak.bond_partners + struct.dihedrals.append(Dihedral(ai, aj, ak, al, improper=improper, type=dihed_type)) + struct.dihedral_types.claim() + +def _process_rbtorsion(struct, force): + """ Adds Ryckaert-Bellemans torsions to the structure """ + typemap = dict() + for ii in range(force.getNumTorsions()): + i, j, k, l, c0, c1, c2, c3, c4, c5 = force.getTorsionParameters(ii) + ai, aj = struct.atoms[i], struct.atoms[j] + ak, al = struct.atoms[k], struct.atoms[l] + key = (c0._value, c1._value, c2._value, c3._value, c4._value, c5._value) + if key in typemap: + dihed_type = typemap[key] + else: + dihed_type = RBTorsionType(c0, c1, c2, c3, c4, c5) + typemap[key] = dihed_type + struct.rb_torsion_types.append(dihed_type) + struct.rb_torsions.append(Dihedral(ai, aj, ak, al, type=dihed_type)) + struct.rb_torsion_types.claim() + +def _process_improper(struct, force): + """ Processes a CustomTorsionForce and looks at the energy expression to see + if it's a quadratic improper torsion. Then adds the parameters if applicable + + Returns + ------- + is_improper : bool + Returns True if the energy expression is recognized as a quadratic + improper, and False otherwise + """ + eqn = force.getEnergyFunction().lower().replace(' ', '') + if ';' in eqn: # Just look at the first segment of the equation if there are multiple + eqn = eqn[:eqn.index(';')] + # Don't try to be fancy with regexes for fear of making a possible mistake. + # ParmEd and OpenMM use only these two eqns for the improper torsions: + # k*(theta-theta0)^2 vs. 0.5*k*(theta-theta0)^2 + # So only recognize the above 2 forms + if eqn not in ('0.5*k*(theta-theta0)^2', 'k*(theta-theta0)^2', 'k*dtheta_torus^2', '0.5*k*dtheta_torus^2'): + return False + if eqn.startswith('0.5'): + fac = 0.5 + else: + fac = 1 + typemap = dict() + for ii in range(force.getNumTorsions()): + # All formulations put k first and equilibrium angle second, in radians + i, j, k, l, (psi_k, psi_eq) = force.getTorsionParameters(ii) + ai, aj = struct.atoms[i], struct.atoms[j] + ak, al = struct.atoms[k], struct.atoms[l] + key = (psi_k, psi_eq) + if key in typemap: + imp_type = typemap[key] + else: + imp_type = ImproperType(psi_k*fac*u.kilojoule_per_mole/u.radian**2, psi_eq*u.radian) + typemap[key] = imp_type + struct.improper_types.append(imp_type) + struct.impropers.append(Improper(ai, aj, ak, al, type=imp_type)) + struct.improper_types.claim() + return True + +def _process_cmap(struct, force): + """ Adds CMAPs to the structure """ + # store the list of cmap types + cmap_types = [] + for ii in range(force.getNumMaps()): + size, grid = force.getMapParameters(ii) + # Future-proof in case units start getting added to these maps + if u.is_quantity(grid): + typ = CmapType(size, grid) # pragma: no cover + else: + typ = CmapType(size, grid*u.kilojoules_per_mole) # pragma: no cover + cmap_types.append(typ) + typ.grid = typ.grid.T.switch_range() + typ.used = False + # Add all cmaps + for ii in range(force.getNumTorsions()): + mapidx, ii, ij, ik, il, ji, jj, jk, jl = force.getTorsionParameters(ii) + if ij != ji or ik != jj or il != jk: + warnings.warn('Non-continuous CMAP torsions detected. Not supported.', OpenMMWarning) + continue + ai, aj, ak = struct.atoms[ii], struct.atoms[ij], struct.atoms[ik] + al, am = struct.atoms[il], struct.atoms[jl] + cmap_type = cmap_types[mapidx] + cmap_type.used = True + struct.cmaps.append(Cmap(ai, aj, ak, al, am, type=cmap_type)) + for cmap_type in cmap_types: + if cmap_type.used: + struct.cmap_types.append(cmap_type) + struct.cmap_types.claim() + +def _process_nonbonded(struct, force, condense_atom_types): + """ Adds nonbonded parameters to the structure """ + typemap = dict() + element_typemap = defaultdict(int) + assert force.getNumParticles() == len(struct.atoms), "Atom # mismatch" + for i in range(force.getNumParticles()): + atom = struct.atoms[i] + chg, sig, eps = force.getParticleParameters(i) + atype_name = atom.type if atom.type != '' else Element[atom.atomic_number] + key = (atype_name, sig._value, eps._value) + if key in typemap and condense_atom_types: + atom_type = typemap[key] + else: + if atom.type == '': + element_typemap[atype_name] += 1 + atype_name = '%s%d' % (atype_name, element_typemap[atype_name]) + typemap[key] = atom_type = AtomType(atype_name, None, atom.mass, atom.atomic_number) + atom.charge = chg.value_in_unit(u.elementary_charge) + rmin = sig.value_in_unit(u.angstroms) * 2**(1/6) / 2 # to rmin/2 + eps = eps.value_in_unit(u.kilocalories_per_mole) + atom_type.set_lj_params(eps, rmin) + atom.atom_type = atom_type + atom.type = atom_type.name + + explicit_exceptions = defaultdict(set) + bond_graph_exceptions = defaultdict(set) + for atom in struct.atoms: + for a2 in atom.bond_partners: + if atom is not a2: + bond_graph_exceptions[atom].add(a2) + for a3 in a2.bond_partners: + if a3 is atom: continue + if atom is not a3: + bond_graph_exceptions[atom].add(a3) + + # TODO should we compress exception types? + for ii in range(force.getNumExceptions()): + i, j, q, sig, eps = force.getExceptionParameters(ii) + q = q.value_in_unit(u.elementary_charge**2) + sig = sig.value_in_unit(u.angstrom) + eps = eps.value_in_unit(u.kilocalorie_per_mole) + ai, aj = struct.atoms[i], struct.atoms[j] + if q == 0 and (sig == 0 or eps == 0): + explicit_exceptions[ai].add(aj) + explicit_exceptions[aj].add(ai) + continue + try: + chgscale = q / (ai.charge * aj.charge) + except ZeroDivisionError: + if q != 0: + raise ValueError(f"Can't scale charge product 0 to match {q}") + chgscale = None + nbtype = NonbondedExceptionType(sig*2**(1/6), eps, chgscale) + struct.adjusts.append(NonbondedException(ai, aj, type=nbtype)) + struct.adjust_types.append(nbtype) + struct.adjust_types.claim() + # Go through all adjust_types and replace any chgscale values that are None with the first + # non-None value present. If all are None, set all to 1.0. This way we maximize the likelihood + # that all generated systems have 1 scaling factor which makes it easier to translate to other + # formats that may not support multiple scaling factors in electrostatic scaling (like GROMACS) + first_scaling_factor = 1.0 + for adjust_type in struct.adjust_types: + if adjust_type.chgscale is not None: + first_scaling_factor = adjust_type.chgscale + break + # Now go through and set all Nones to first_scaling_factor + for adjust_type in struct.adjust_types: + if adjust_type.chgscale is None: + adjust_type.chgscale = first_scaling_factor + + # Check that all of our exceptions are accounted for + for ai, exceptions in bond_graph_exceptions.items(): + if exceptions - explicit_exceptions[ai]: + struct.unknown_functional = True + warnings.warn('Detected incomplete exceptions. Not supported.', OpenMMWarning) + break diff --git a/pmx/workflow/parmed/openmm/utils.py b/pmx/workflow/parmed/openmm/utils.py new file mode 100644 index 00000000..225b2a7a --- /dev/null +++ b/pmx/workflow/parmed/openmm/utils.py @@ -0,0 +1,110 @@ +""" +This package contains some useful functionality for common tasks in OpenMM +""" +from .. import unit as u +from ..utils.decorators import needs_openmm + +def energy_decomposition(structure, context, nrg=u.kilocalories_per_mole): + """ + This computes the energy of every force group in the given structure and + computes the energy for each force group for the given Context. Note, the + context must have positions already assigned. + + Parameters + ---------- + structure : Structure + This should be the Structure object from which the System object in the + Context was created + context : mm.Context + The OpenMM context set up for computing forces and energies + nrg : energy unit, optional + The unit to convert all energies into. Default is kcal/mol + + Returns + ------- + dict {str:float} + A dictionary mapping the name of the force group (taken from the + attribute names of the format XXX_FORCE_GROUP in the structure object) + with the energy of that group in + """ + all_names = dict() + force_group_names = dict() + energy_components = dict() + + for attr in dir(structure): + if attr.endswith('_FORCE_GROUP'): + val = getattr(structure, attr) + all_names[val] = attr.replace('_FORCE_GROUP', '').lower() + + for force in context.getSystem().getForces(): + gp = force.getForceGroup() + force_group_names[gp] = all_names[gp] + + for grp, name in force_group_names.items(): + state = context.getState(getEnergy=True, groups=1<') + +class XmlFile(metaclass=FileFormatType): + """ + Wrapper for parsing OpenMM-serialized objects. Supports serialized State, + System, Integrator, and ForceField objects. + """ + + @staticmethod + def id_format(filename): + """ Identifies the file type as an XML file + + Parameters + ---------- + filename : str + Name of the file to check format for + + Returns + ------- + is_fmt : bool + True if it is an XML format, False otherwise + """ + with closing(genopen(filename, 'r')) as f: + for line in f: + line = line.strip() + if not line: + continue + rematch = _xmlre.match(line) + if not rematch: + return False + stuff = rematch.groups()[0] + if stuff[0] in '?!': + continue + kind = stuff.split()[0] + if kind in ('System', 'State', 'ForceField', 'Integrator'): + return True + return False + + @staticmethod + @needs_openmm + def parse(filename): + """ + Parses XML file and returns deserialized object. The return value + depends on the serialized object, summarized below + + - System : returns openmm.System + - State : returns openmm.State + - Integrator : returns openmm.Integrator subclass + - ForceField : returns openmm.app.ForceField + + Parameters + ---------- + filename : str or file-like + The file name or file object containing the XML-serialized object + + Returns + ------- + obj : System, State, Integrator, or ForceField + The deserialized object + + Notes + ----- + OpenMM requires the entire contents of this file read into memory. As a + result, this function may require a significant amount of memory. + """ + import openmm as mm + from openmm import app + if isinstance(filename, str): + with closing(genopen(filename, 'r')) as f: + contents = f.read() + else: + contents = filename.read() + # ForceField is not handled by XmlSerializer + if ' 0 + for urey in struct.urey_bradleys: + if urey.type is None or urey.type is NoUreyBradley: continue + key = _find_ureybrad_key(urey) + if key is None: continue + if urey_brads_preassigned and key not in params.urey_bradley_types: + warnings.warn('Angle corresponding to Urey-Bradley type not found', ParameterWarning) + typ = copy(urey.type) + params.urey_bradley_types[key] = typ + params.urey_bradley_types[tuple(reversed(key))] = typ + if not urey_brads_preassigned and len(params.urey_bradley_types) > 0: + # Go through all of our angle parameters and make sure there is a + # matching Urey-Bradley list. If there's not, that means there is no + # Urey-Bradley term for that angle + for key in params.angle_types: + if key in params.urey_bradley_types: continue + params.urey_bradley_types[key] = NoUreyBradley + for adjust in struct.adjusts: + if adjust.type is None: continue + key = (adjust.atom1.type, adjust.atom2.type) + if key in params.pair_types: + if not allow_unequal_duplicates and params.pair_types[key] != adjust.type: + raise ParameterError('Unequal pair types defined between {} and {}'.format(*key)) + continue # pragma: no cover + typ = copy(adjust.type) + params.pair_types[key] = typ + params.pair_types[tuple(reversed(key))] = typ + # Trap for Amoeba potentials + if (struct.trigonal_angles or struct.out_of_plane_bends or struct.torsion_torsions or + struct.stretch_bends or struct.trigonal_angles or struct.pi_torsions): + raise NotImplementedError('Cannot extract parameters from an Amoeba-parametrized system') + return params + + def condense(self, do_dihedrals=True): + """ + This function goes through each of the parameter type dicts and + eliminates duplicate types. After calling this function, every unique + bond, angle, dihedral, improper, or cmap type will pair with EVERY key + in the type mapping dictionaries that points to the equivalent type + + Parameters + ---------- + do_dihedrals : bool=True + Dihedrals can take the longest time to compress since testing their + equality takes the longest (this is complicated by the existence of + multi-term torsions). This flag will allow you to *skip* condensing + the dihedral parameter types (for large parameter sets, this can cut + the compression time in half) + + Returns + ------- + self + The instance that is being condensed + + Notes + ----- + The return value allows you to condense the types at construction time. + + Example + ------- + >>> params = ParameterSet().condense() + >>> params + + """ + # First scan through all of the bond types + self._condense_types(self.bond_types) + self._condense_types(self.angle_types) + self._condense_types(self.urey_bradley_types) + if do_dihedrals: + self._condense_types(self.dihedral_types) + self._condense_types(self.rb_torsion_types) + self._condense_types(self.improper_periodic_types) + self._condense_types(self.improper_types) + self._condense_types(self.cmap_types) + return self + + @staticmethod + def _condense_types(typedict): + """ + Loops through the given dict and condenses all types. + + Parameter + --------- + typedict : dict + Type dictionary to condense + """ + keylist = list(typedict.keys()) + for i in range(len(keylist) - 1): + key1 = keylist[i] + for j in range(i+1, len(keylist)): + key2 = keylist[j] + if typedict[key1] == typedict[key2]: + typedict[key2] = typedict[key1] + + @property + def combining_rule(self): + return self._combining_rule + + @combining_rule.setter + def combining_rule(self, value): + if value not in ('lorentz', 'geometric'): + raise ValueError('combining_rule must be "lorentz" or "geometric"') + self._combining_rule = value + + def typeify_templates(self): + """ Assign atom types to atom names in templates """ + from .modeller import ResidueTemplateContainer, ResidueTemplate + for residue in self.residues.values(): + if isinstance(residue, ResidueTemplateContainer): + for res in residue: + for atom in res: + atom.atom_type = self.atom_types[atom.type] + else: + assert isinstance(residue, ResidueTemplate), 'Wrong type!' + for atom in residue: + atom.atom_type = self.atom_types[atom.type] + +def _find_ureybrad_key(urey): + """ + Finds a key for a given Urey-Bradley by finding the middle atom in an angle. + Raises a ParameterWarning if no middle atom found + """ + a1, a2 = urey.atom1, urey.atom2 + shared_bond_partners = set(a1.bond_partners) & set(a2.bond_partners) + if len({a.type for a in shared_bond_partners}) != 1: + warnings.warn(f'Urey-Bradley {repr(urey)} shares multiple central atoms', ParameterWarning) + return (a1.type, list(shared_bond_partners)[0].type, a2.type) + +def _find_improper_keys(dih): + """ Finds the central atom (i.e., that bonded to everything else) """ + assert dih.improper, 'Should not be called on non-improper!' + all_atoms = {dih.atom1, dih.atom2, dih.atom3, dih.atom4} + for atom in all_atoms: + for oatom in all_atoms: + if oatom is atom: + continue + if oatom not in atom.bond_partners: + break + else: + # This *is* the central atom + for key in permutations([a.type for a in all_atoms if a is not atom]): + yield (key[0], key[1], atom.type, key[2]) + return # break out of the generator + # If we got here, we found no central atom. *assume* it's the third spot already... + for key in permutations([dih.atom1.type, dih.atom2.type, dih.atom4.type]): + yield (key[0], key[1], dih.atom3.type, key[2]) diff --git a/pmx/workflow/parmed/periodic_table.py b/pmx/workflow/parmed/periodic_table.py new file mode 100644 index 00000000..51465d80 --- /dev/null +++ b/pmx/workflow/parmed/periodic_table.py @@ -0,0 +1,306 @@ +""" +Contains all of the elements in the periodic table and dictionaries that +store all of the data found in the periodic table for that element, indexed +by the element's symbol. For consistency with AMBER, a fictitious element +'EP' is added to all of the arrays that is just an Extra Point, with no mass +or any other meaningful attribute. It's just a container to put an extra +charge +""" +# Data descriptions: +# +# KNOWN_ELEMENTS: number of known elements +# Element: array whose indices are the atomic number corresponding to +# the element +# AtomicNum: dictionary matching chemical symbol to atomic number +# Mass: dictionary matching chemical symbol to atomic mass +# Name: dicionary matching chemical symbol to their full name +# OriginName: dictionary matching chemical symbol to the name from which +# the symbol comes +# Phase: Lists standard phase that the element appears in + +KNOWN_ELEMENTS = 118 + +AtomicNum = { 'H' : 1, 'He' : 2, 'Li' : 3, 'Be' : 4, 'B' : 5, 'C' : 6, + 'N' : 7, 'O' : 8, 'F' : 9, 'Ne' : 10, 'Na' : 11, 'Mg' : 12, + 'Al' : 13, 'Si' : 14, 'P' : 15, 'S' : 16, 'Cl' : 17, 'Ar' : 18, + 'K' : 19, 'Ca' : 20, 'Sc' : 21, 'Ti' : 22, 'V' : 23, 'Cr' : 24, + 'Mn' : 25, 'Fe' : 26, 'Co' : 27, 'Ni' : 28, 'Cu' : 29, 'Zn' : 30, + 'Ga' : 31, 'Ge' : 32, 'As' : 33, 'Se' : 34, 'Br' : 35, 'Kr' : 36, + 'Rb' : 37, 'Sr' : 38, 'Y' : 39, 'Zr' : 40, 'Nb' : 41, 'Mo' : 42, + 'Tc' : 43, 'Ru' : 44, 'Rh' : 45, 'Pd' : 46, 'Ag' : 47, 'Cd' : 48, + 'In' : 49, 'Sn' : 50, 'Sb' : 51, 'Te' : 52, 'I' : 53, 'Xe' : 54, + 'Cs' : 55, 'Ba' : 56, 'La' : 57, 'Ce' : 58, 'Pr' : 59, 'Nd' : 60, + 'Pm' : 61, 'Sm' : 62, 'Eu' : 63, 'Gd' : 64, 'Tb' : 65, 'Dy' : 66, + 'Ho' : 67, 'Er' : 68, 'Tm' : 69, 'Yb' : 70, 'Lu' : 71, 'Hf' : 72, + 'Ta' : 73, 'W' : 74, 'Re' : 75, 'Os' : 76, 'Ir' : 77, 'Pt' : 78, + 'Au' : 79, 'Hg' : 80, 'Tl' : 81, 'Pb' : 82, 'Bi' : 83, 'Po' : 84, + 'At' : 85, 'Rn' : 86, 'Fr' : 87, 'Ra' : 88, 'Ac' : 89, 'Th' : 90, + 'Pa' : 91, 'U' : 92, 'Np' : 93, 'Pu' : 94, 'Am' : 95, 'Cm' : 96, + 'Bk' : 97, 'Cf' : 98, 'Es' : 99, 'Fm' :100, 'Md' :101, 'No' :102, + 'Lr' :103, 'Rf' :104, 'Db' :105, 'Sg' :106, 'Bh' :107, 'Hs' :108, + 'Mt' :109, 'Ds' :110, 'Rg' :111, 'Cn' :112, 'Nh' :113, 'Fl' :114, + 'Mc' :115, 'Lv' :116, 'Ts' :117, 'Og' :118, 'EP' : 0 , 'LP' : 0, + 'Lp' : 0, 'Ep' : 0} + +Element = [ 'EP', + 'H' ,'He','Li','Be','B' ,'C' ,'N' ,'O' ,'F' ,'Ne','Na','Mg', + 'Al','Si','P' ,'S' ,'Cl','Ar','K' ,'Ca','Sc','Ti','V' ,'Cr', + 'Mn','Fe','Co','Ni','Cu','Zn','Ga','Ge','As','Se','Br','Kr', + 'Rb','Sr','Y' ,'Zr','Nb','Mo','Tc','Ru','Rh','Pd','Ag','Cd', + 'In','Sn','Sb','Te','I' ,'Xe','Cs','Ba','La','Ce','Pr','Nd', + 'Pm','Sm','Eu','Gd','Tb','Dy','Ho','Er','Tm','Yb','Lu','Hf', + 'Ta','W' ,'Re','Os','Ir','Pt','Au','Hg','Tl','Pb','Bi','Po', + 'At','Rn','Fr','Ra','Ac','Th','Pa','U' ,'Np','Pu','Am','Cm', + 'Bk','Cf','Es','Fm','Md','No','Lr','Rf','Db','Sg','Bh','Hs', + 'Mt','Ds','Rg','Cn','Nh','Fl','Mc','Lv','Ts','Og' ] + +Mass = { 'H' : 1.0079 , 'He' : 4.0026 , 'Li' : 6.941 , + 'Be' : 9.0122 , 'B' : 10.811 , 'C' : 12.0107 , + 'N' : 14.0067 , 'O' : 15.9994 , 'F' : 18.9984 , + 'Ne' : 20.1797 , 'Na' : 22.9898 , 'Mg' : 24.3050 , + 'Al' : 26.9815 , 'Si' : 28.0855 , 'P' : 30.9738 , + 'S' : 32.065 , 'Cl' : 35.453 , 'Ar' : 39.948 , + 'K' : 39.0983 , 'Ca' : 40.078 , 'Sc' : 44.9559 , + 'Ti' : 47.867 , 'V' : 50.9415 , 'Cr' : 51.9961 , + 'Mn' : 54.9380 , 'Fe' : 55.845 , 'Co' : 58.9331 , + 'Ni' : 58.6934 , 'Cu' : 63.546 , 'Zn' : 65.409 , + 'Ga' : 69.723 , 'Ge' : 72.64 , 'As' : 74.9216 , + 'Se' : 78.96 , 'Br' : 79.904 , 'Kr' : 83.798 , + 'Rb' : 85.4678 , 'Sr' : 87.62 , 'Y' : 88.9059 , + 'Zr' : 91.224 , 'Nb' : 92.9064 , 'Mo' : 95.94 , + 'Tc' : 98. , 'Ru' : 101.07 , 'Rh' : 102.9055 , + 'Pd' : 106.42 , 'Ag' : 107.8682 , 'Cd' : 112.411 , + 'In' : 114.818 , 'Sn' : 118.710 , 'Sb' : 121.760 , + 'Te' : 127.60 , 'I' : 126.9045 , 'Xe' : 131.293 , + 'Cs' : 132.9055 , 'Ba' : 137.327 , 'La' : 138.9055 , + 'Ce' : 140.116 , 'Pr' : 140.9077 , 'Nd' : 144.242 , + 'Pm' : 145. , 'Sm' : 150.36 , 'Eu' : 151.964 , + 'Gd' : 157.25 , 'Tb' : 158.9254 , 'Dy' : 162.500 , + 'Ho' : 164.9303 , 'Er' : 167.259 , 'Tm' : 168.9342 , + 'Yb' : 173.04 , 'Lu' : 174.967 , 'Hf' : 178.49 , + 'Ta' : 180.9479 , 'W' : 183.84 , 'Re' : 186.207 , + 'Os' : 190.23 , 'Ir' : 192.217 , 'Pt' : 195.084 , + 'Au' : 196.9666 , 'Hg' : 200.59 , 'Tl' : 204.3833 , + 'Pb' : 207.2 , 'Bi' : 208.9804 , 'Po' : 209. , + 'At' : 210. , 'Rn' : 222. , 'Fr' : 223. , + 'Ra' : 226. , 'Ac' : 227. , 'Th' : 232.0381 , + 'Pa' : 231.0359 , 'U' : 238.0289 , 'Np' : 237. , + 'Pu' : 244. , 'Am' : 243. , 'Cm' : 247. , + 'Bk' : 247. , 'Cf' : 251. , 'Es' : 252. , + 'Fm' : 257. , 'Md' : 258. , 'No' : 259. , + 'Lr' : 262. , 'Rf' : 261. , 'Db' : 262. , + 'Sg' : 266. , 'Bh' : 264. , 'Hs' : 277. , + 'Mt' : 268. , 'Ds' : 281. , 'Rg' : 272. , + 'Cn' : 285. , 'Nh' : 286. , 'Fl' : 289. , + 'Mc' : 289. , 'Lv' : 293. , 'Ts' : 294. , + 'Og' : 294. , 'EP' : 0.000000 } + +Name = { 'H' : 'Hydrogen' ,'He' : 'Helium' ,'Li' : 'Lithium' , + 'Be' : 'Beryllium' ,'B' : 'Boron' ,'C' : 'Carbon' , + 'N' : 'Nitrogen' ,'O' : 'Oxygen' ,'F' : 'Fluorine' , + 'Ne' : 'Neon' ,'Na' : 'Sodium' ,'Mg' : 'Magnesium' , + 'Al' : 'Aluminum' ,'Si' : 'Silicon' ,'P' : 'Phosphorus' , + 'S' : 'Sulfur' ,'Cl' : 'Chlorine' ,'Ar' : 'Argon' , + 'K' : 'Potassium' ,'Ca' : 'Calcium' ,'Sc' : 'Scandium' , + 'Ti' : 'Titanium' ,'V' : 'Vanadium' ,'Cr' : 'Chromium' , + 'Mn' : 'Manganese' ,'Fe' : 'Iron' ,'Co' : 'Cobalt' , + 'Ni' : 'Nickel' ,'Cu' : 'Copper' ,'Zn' : 'Zinc' , + 'Ga' : 'Gallium' ,'Ge' : 'Germanium' ,'As' : 'Arsenic' , + 'Se' : 'Selenium' ,'Br' : 'Bromine' ,'Kr' : 'Krypton' , + 'Rb' : 'Rubidium' ,'Sr' : 'Strontium' ,'Y' : 'Yttrium' , + 'Zr' : 'Zirconium' ,'Nb' : 'Niobium' ,'Mo' : 'Molybdenum' , + 'Tc' : 'Technetium' ,'Ru' : 'Ruthenium' ,'Rh' : 'Rhodium' , + 'Pd' : 'Palladium' ,'Ag' : 'Silver' ,'Cd' : 'Cadmium' , + 'In' : 'Indium' ,'Sn' : 'Tin' ,'Sb' : 'Antimony' , + 'Te' : 'Tellurium' ,'I' : 'Iodine' ,'Xe' : 'Xenon' , + 'Cs' : 'Cesium' ,'Ba' : 'Barium' ,'La' : 'Lanthanum' , + 'Ce' : 'Cerium' ,'Pr' : 'Praseodymium' ,'Nd' : 'Neodymium' , + 'Pm' : 'Promethium' ,'Sm' : 'Samarium' ,'Eu' : 'Europium' , + 'Gd' : 'Gadolinium' ,'Tb' : 'Terbium' ,'Dy' : 'Dysprosium' , + 'Ho' : 'Holmium' ,'Er' : 'Erbium' ,'Tm' : 'Thulium' , + 'Yb' : 'Ytterbium' ,'Lu' : 'Lutetium' ,'Hf' : 'Hafnium' , + 'Ta' : 'Tantalum' ,'W' : 'Tungsten' ,'Re' : 'Rhenium' , + 'Os' : 'Osmium' ,'Ir' : 'Iridium' ,'Pt' : 'Platinum' , + 'Au' : 'Gold' ,'Hg' : 'Mercury' ,'Tl' : 'Thallium' , + 'Pb' : 'Lead' ,'Bi' : 'Bismuth' ,'Po' : 'Polonium' , + 'At' : 'Astatine' ,'Rn' : 'Radon' ,'Fr' : 'Francium' , + 'Ra' : 'Radium' ,'Ac' : 'Actinium' ,'Th' : 'Thorium' , + 'Pa' : 'Proactinium' ,'U' : 'Uranium' ,'Np' : 'Neptunium' , + 'Pu' : 'Plutonium' ,'Am' : 'Americium' ,'Cm' : 'Curium' , + 'Bk' : 'Berkelium' ,'Cf' : 'Californium' ,'Es' : 'Einsteinium' , + 'Fm' : 'Fermium' ,'Md' : 'Mendelevium' ,'No' : 'Nobelium' , + 'Lr' : 'Lawrencium' ,'Rf' : 'Rutherfordium','Db' : 'Dubnium' , + 'Sg' : 'Seaborgium' ,'Bh' : 'Bohrium' ,'Hs' : 'Hassium' , + 'Mt' : 'Meitnerium' ,'Ds' : 'Darmstadtium' ,'Rg' : 'Roentgenium' , + 'Cn' : 'Copernicium' ,'Nh' : 'Nihonium' ,'Fl' : 'Flerovium' , + 'Mc' : 'Moscovium' ,'Lv' : 'Livermorium' ,'Ts' : 'Tennessine' , + 'Og' : 'Oganesson' ,'EP' : 'Extra Point' ,'LP' : 'Extra Point' , + 'Ep' : 'Extra Point' ,'Lp' : 'Extra Point'} + +OriginName = { + 'H' : 'Hydrogen' ,'He' : 'Helium' ,'Li' : 'Lithium' , + 'Be' : 'Beryllium' ,'B' : 'Boron' ,'C' : 'Carbon' , + 'N' : 'Nitrogen' ,'O' : 'Oxygen' ,'F' : 'Fluorine' , + 'Ne' : 'Neon' ,'Na' : 'Natrium' ,'Mg' : 'Magnesium' , + 'Al' : 'Aluminum' ,'Si' : 'Silicon' ,'P' : 'Phosphorus' , + 'S' : 'Sulfur' ,'Cl' : 'Chlorine' ,'Ar' : 'Argon' , + 'K' : 'Kalium' ,'Ca' : 'Calcium' ,'Sc' : 'Scandium' , + 'Ti' : 'Titanium' ,'V' : 'Vanadium' ,'Cr' : 'Chromium' , + 'Mn' : 'Manganese' ,'Fe' : 'Ferrum' ,'Co' : 'Cobalt' , + 'Ni' : 'Nickel' ,'Cu' : 'Cuprum' ,'Zn' : 'Zinc' , + 'Ga' : 'Gallium' ,'Ge' : 'Germanium' ,'As' : 'Arsenic' , + 'Se' : 'Selenium' ,'Br' : 'Bromine' ,'Kr' : 'Krypton' , + 'Rb' : 'Rubidium' ,'Sr' : 'Strontium' ,'Y' : 'Yttrium' , + 'Zr' : 'Zirconium' ,'Nb' : 'Niobium' ,'Mo' : 'Molybdenum' , + 'Tc' : 'Technetium' ,'Ru' : 'Ruthenium' ,'Rh' : 'Rhodium' , + 'Pd' : 'Palladium' ,'Ag' : 'Argentum' ,'Cd' : 'Cadmium' , + 'In' : 'Indium' ,'Sn' : 'Stannum' ,'Sb' : 'Stibium' , + 'Te' : 'Tellurium' ,'I' : 'Iodine' ,'Xe' : 'Xenon' , + 'Cs' : 'Cesium' ,'Ba' : 'Barium' ,'La' : 'Lanthanum' , + 'Ce' : 'Cerium' ,'Pr' : 'Praseodymium' ,'Nd' : 'Neodymium' , + 'Pm' : 'Promethium' ,'Sm' : 'Samarium' ,'Eu' : 'Europium' , + 'Gd' : 'Gadolinium' ,'Tb' : 'Terbium' ,'Dy' : 'Dysprosium' , + 'Ho' : 'Holmium' ,'Er' : 'Erbium' ,'Tm' : 'Thulium' , + 'Yb' : 'Ytterbium' ,'Lu' : 'Lutetium' ,'Hf' : 'Hafnium' , + 'Ta' : 'Tantalum' ,'W' : 'Wolfram' ,'Re' : 'Rhenium' , + 'Os' : 'Osmium' ,'Ir' : 'Iridium' ,'Pt' : 'Platinum' , + 'Au' : 'Aurum' ,'Hg' : 'Hydrargyrum' ,'Tl' : 'Thallium' , + 'Pb' : 'Plumbum' ,'Bi' : 'Bismuth' ,'Po' : 'Polonium' , + 'At' : 'Astatine' ,'Rn' : 'Radon' ,'Fr' : 'Francium' , + 'Ra' : 'Radium' ,'Ac' : 'Actinium' ,'Th' : 'Thorium' , + 'Pa' : 'Proactinium','U' : 'Uranium' ,'Np' : 'Neptunium' , + 'Pu' : 'Plutonium' ,'Am' : 'Americium' ,'Cm' : 'Curium' , + 'Bk' : 'Berkelium' ,'Cf' : 'Californium' ,'Es' : 'Einsteinium' , + 'Fm' : 'Fermium' ,'Md' : 'Mendelevium' ,'No' : 'Nobelium' , + 'Lr' : 'Lawrencium' ,'Rf' : 'Rutherfordium','Db' : 'Dubnium' , + 'Sg' : 'Seaborgium' ,'Bh' : 'Bohrium' ,'Hs' : 'Hassium' , + 'Mt' : 'Meitnerium' ,'Ds' : 'Darmstadtium' ,'Rg' : 'Roentgenium' , + 'Cn' : 'Copernicium','Nh' : 'Nihonium' ,'Fl' : 'Flerovium' , + 'Mc' : 'Moscovium' ,'Lv' : 'Livermorium' ,'Ts' : 'Tennessine' , + 'Og' : 'Oganesson' ,'EP' : 'Extra Point' ,'LP' : 'Extra Point' , + 'Ep' : 'Extra Point','Lp' : 'Extra Point' +} + +Phase = { 'H' : 'Gas' ,'He' : 'Gas' ,'Li' : 'Solid' , + 'Be' : 'Solid' ,'B' : 'Solid' ,'C' : 'Solid' , + 'N' : 'Gas' ,'O' : 'Gas' ,'F' : 'Gas' , + 'Ne' : 'Gas' ,'Na' : 'Solid' ,'Mg' : 'Solid' , + 'Al' : 'Solid' ,'Si' : 'Solid' ,'P' : 'Solid' , + 'S' : 'Solid' ,'Cl' : 'Gas' ,'Ar' : 'Gas' , + 'K' : 'Solid' ,'Ca' : 'Solid' ,'Sc' : 'Solid' , + 'Ti' : 'Solid' ,'V' : 'Solid' ,'Cr' : 'Solid' , + 'Mn' : 'Solid' ,'Fe' : 'Solid' ,'Co' : 'Solid' , + 'Ni' : 'Solid' ,'Cu' : 'Solid' ,'Zn' : 'Solid' , + 'Ga' : 'Solid' ,'Ge' : 'Solid' ,'As' : 'Solid' , + 'Se' : 'Solid' ,'Br' : 'Liquid' ,'Kr' : 'Gas' , + 'Rb' : 'Solid' ,'Sr' : 'Solid' ,'Y' : 'Solid' , + 'Zr' : 'Solid' ,'Nb' : 'Solid' ,'Mo' : 'Solid' , + 'Tc' : 'Solid' ,'Ru' : 'Solid' ,'Rh' : 'Solid' , + 'Pd' : 'Solid' ,'Ag' : 'Solid' ,'Cd' : 'Solid' , + 'In' : 'Solid' ,'Sn' : 'Solid' ,'Sb' : 'Solid' , + 'Te' : 'Solid' ,'I' : 'Solid' ,'Xe' : 'Gas' , + 'Cs' : 'Solid' ,'Ba' : 'Solid' ,'La' : 'Solid' , + 'Ce' : 'Solid' ,'Pr' : 'Solid' ,'Nd' : 'Solid' , + 'Pm' : 'Solid' ,'Sm' : 'Solid' ,'Eu' : 'Solid' , + 'Gd' : 'Solid' ,'Tb' : 'Solid' ,'Dy' : 'Solid' , + 'Ho' : 'Solid' ,'Er' : 'Solid' ,'Tm' : 'Solid' , + 'Yb' : 'Solid' ,'Lu' : 'Solid' ,'Hf' : 'Solid' , + 'Ta' : 'Solid' ,'W' : 'Solid' ,'Re' : 'Solid' , + 'Os' : 'Solid' ,'Ir' : 'Solid' ,'Pt' : 'Solid' , + 'Au' : 'Solid' ,'Hg' : 'Liquid' ,'Tl' : 'Solid' , + 'Pb' : 'Solid' ,'Bi' : 'Solid' ,'Po' : 'Solid' , + 'At' : 'Solid' ,'Rn' : 'Solid' ,'Fr' : 'Solid' , + 'Ra' : 'Gas' ,'Ac' : 'Solid' ,'Th' : 'Solid' , + 'Pa' : 'Solid' ,'U' : 'Solid' ,'Np' : 'Solid' , + 'Pu' : 'Solid' ,'Am' : 'Solid' ,'Cm' : 'Solid' , + 'Bk' : 'Solid' ,'Cf' : 'Solid' ,'Es' : 'Solid' , + 'Fm' : 'Solid' ,'Md' : 'Solid' ,'No' : 'Solid' , + 'Lr' : 'Solid' ,'Rf' : 'Unknown' ,'Db' : 'Unknown' , + 'Sg' : 'Unknown' ,'Bh' : 'Unknown' ,'Hs' : 'Unknown' , + 'Mt' : 'Unknown' ,'Ds' : 'Unknown' ,'Rg' : 'Unknown' , + 'Cn' : 'Unknown' ,'Nh' : 'Unknown' ,'Fl' : 'Unknown' , + 'Mc' : 'Unknown' ,'Lv' : 'Unknown' ,'Ts' : 'Unknown' , + 'Og' : 'Unknown' ,'EP' : 'N/A' ,'Ep' : 'N/A' , + 'LP' : 'N/A' ,'Lp' : 'N/A' +} + +_sorted_masses = sorted(Mass.items(), key=lambda x: x[1]) + +def element_by_mass(mass): + """ + Determine the element that has a mass closest to the input mass + + Parameters + ---------- + mass : float + The atomic mass to compare to + + Returns + ------- + element: str + The returned string is the name of the element whose atomic mass is + closest to the input mass + + Notes + ----- + This actually fails (i.e., produces poor predictions) for some cases when + masses have changed -- particularly in the case of Hydrogen mass + repartitioning. + """ + diff = mass + 1 + best_guess = 'EP' + + for element, element_mass in _sorted_masses: + d = abs(element_mass - mass) + if d < diff: + best_guess = element + diff = d + else: + break + + return best_guess + +def element_by_name(name): + """ + Determine the element based on the name of an atom. This is very naive. It + first tries to match the first letter of the element. If that doesn't work, + it tries to match the first *two* letters. If that still doesn't work, it + defaults to an extra point + + Parameters + ---------- + name : str + Name of the atom to determine an element for + + Returns + ------- + element : str + The name of the best-matching element + + Notes + ----- + This should be a last-case scenario for guessing element information. For + instance, Ca will never be matched, since calcium atoms will be tagged as + carbon before the second letter is tried. This is usually OK for + biomolecules, but you are better off using the mass or, even better, an + appropriate representation of the atomic number to begin with + """ + name = name.strip() + if len(name) == 0: + return Element[0] + try: + atomic_number = AtomicNum[name[0].upper()] + except KeyError: + sym = name[:2] + try: + sym = '%s%s' % (sym[0].upper(), sym[1].lower()) + atomic_number = AtomicNum[sym] + except (KeyError, IndexError): + atomic_number = 0 # give up + + return Element[atomic_number] + +# Add some mass aliases here. We need to do it *after* _sorted_masses is created above, since +# the element_by_mass routine which uses the _sorted_masses assumes that the masses are all +# monotonically strictly increasing +Mass.update(dict(Ep=0.0, LP=0.0, Lp=0.0)) diff --git a/pmx/workflow/parmed/rdkit/__init__.py b/pmx/workflow/parmed/rdkit/__init__.py new file mode 100644 index 00000000..a4007245 --- /dev/null +++ b/pmx/workflow/parmed/rdkit/__init__.py @@ -0,0 +1,10 @@ +""" +A package dealing with RDKit integration. +""" +from .rdkit import RDKit + +__all__ = ['load_rdkit', 'from_smiles', 'from_sdf'] + +load_rdkit = RDKit.load +from_smiles = RDKit.from_smiles +from_sdf = RDKit.from_sdf diff --git a/pmx/workflow/parmed/rdkit/rdkit.py b/pmx/workflow/parmed/rdkit/rdkit.py new file mode 100644 index 00000000..00986fa0 --- /dev/null +++ b/pmx/workflow/parmed/rdkit/rdkit.py @@ -0,0 +1,80 @@ +""" This package contains classes responsible for loading rdkit objects """ +from io import StringIO +from ..formats import PDBFile + +class RDKit: + + @staticmethod + def load(rmol): + """ + Load a :class:`Mol` object and return a populated :class:`Structure` + instance + + Parameters + ---------- + rmol: :class:`Mol` + RDKit :class:`Mol` object to convert + + Examples + -------- + >>> from rdkit import Chem + >>> import parmed as pmd + >>> mol = Chem.MolFromSmiles('Cc1ccccc1') + >>> struct = pmd.load_rdkit(mol) + """ + from rdkit import Chem + fh = StringIO(Chem.MolToPDBBlock(rmol)) + return PDBFile.parse(fh) + + @staticmethod + def from_smiles(smiles, coordinates=True, hydrogens=True): + """ + Load smiles string to :class:`Structure` + + Parameters + ---------- + smiles : str, smiles + coordinates : bool, default True + if True, use `rdkit.Chem.AllChem.EmbedMultipleConfs to assign coordinates + hydrogens : bool, default True + if True, use `rdkit.Chem.AddHs` to generate explicit hydrogens + + Returns + ------- + parm : :class:`Structure` + """ + from rdkit import Chem + from rdkit.Chem import AllChem + mol = Chem.MolFromSmiles(smiles) + + if hydrogens: + mol = Chem.AddHs(mol) + + if coordinates: + AllChem.EmbedMultipleConfs(mol, useExpTorsionAnglePrefs=True, useBasicKnowledge=True) + + parm = RDKit.load(mol) + if not coordinates: + parm.coordinates = None + parm._coordinates = None + return parm + + @staticmethod + def from_sdf(filename, structure=False): + """ + Load SDF file to :class:`Structure` + + Parameters + ---------- + filename: str + structure : bool, default False + if True, return a :class:`Structure` + if False, return a list of :class:`Structure` + """ + from rdkit import Chem + sdf_collection = Chem.SDMolSupplier(filename, removeHs=False) + if structure: + mol = next(sdf_collection) + return RDKit.load(mol) + else: + return [RDKit.load(mol) for mol in sdf_collection] diff --git a/pmx/workflow/parmed/residue.py b/pmx/workflow/parmed/residue.py new file mode 100644 index 00000000..729ba9e5 --- /dev/null +++ b/pmx/workflow/parmed/residue.py @@ -0,0 +1,286 @@ +""" +This module contains basic information and functionality related to individual +residues in typical biopolymers. +""" +from abc import ABC + +__all__ = ['AminoAcidResidue', 'RNAResidue', 'DNAResidue', 'ALA', 'ARG', 'ASN', + 'ASP', 'CYS', 'GLU', 'GLN', 'GLY', 'HIS', 'HYP', 'ILE', 'LEU', 'LYS', + 'MET', 'PHE', 'PRO', 'SER', 'THR', 'TRP', 'TYR', 'VAL', 'DA', 'DT', + 'DG', 'DC', 'A', 'U', 'G', 'C', 'SOLVENT_NAMES', 'EXTRA_POINT_NAMES', + 'CATION_NAMES', 'ANION_NAMES', 'ALLION_NAMES'] + +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +class BiomolecularResidue(ABC): + """ Base class for different classes of biopolymer residues """ + _all_residues_by_name = dict() + _all_residues_by_abbr = dict() + _all_residues_by_symbol = dict() + all_residues = [] + + def __init_(self, *args, **kwargs): + super().__init__() + + @classmethod + def get(cls, key): + raise NotImplementedError() + + def __str__(self): + return self.name + + @classmethod + def has(cls, thing): + """ + Determines if a particular BiomolecularResidue or residue name is + present in this classification of biomolecular residues + + Parameters + ---------- + thing : str or :class:`BiomolecularResidue` + + Returns + ------- + contains : bool + If the residue or residue name *is* of this type, True. Otherwise, + False. + """ + if isinstance(thing, BiomolecularResidue): + return thing in cls.all_residues + try: + cls.get(thing) + except KeyError: + return False + else: + return True + +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +class AminoAcidResidue(BiomolecularResidue): + """ + An individual amino acid residue. + + Parameters + ---------- + name : str + The name of the residue + abbr : str + The 3-letter abbreviation of the amino acid residue + symbol : str + The 1-letter symbol of the amino acid + aliases : list of str, optional + A list of other abbreviations that *also* refer to this residue + + Raises + ------ + ValueError + If any aliases have the same abbreviation as *other* + """ + _all_residues_by_name = dict() + _all_residues_by_abbr = dict() + _all_residues_by_symbol = dict() + all_residues = [] + + def __init__(self, name, abbr, symbol, aliases=None): + self.name = name + self.abbr = abbr + self.symbol = symbol + type(self)._all_residues_by_name[name.upper()] = self + type(self)._all_residues_by_abbr[abbr.upper()] = self + if symbol is not None: + type(self)._all_residues_by_symbol[symbol.upper()] = self + type(self).all_residues.append(self) + if aliases is not None: + for alias in aliases: + alias = alias.upper() + if alias in type(self)._all_residues_by_abbr: + raise ValueError('%s is already an abbreviation' % alias) + type(self)._all_residues_by_abbr[alias] = self + + def __repr__(self): + return '' % (self.name, self.abbr, + self.symbol) + + @classmethod + def get(cls, key, abbronly=False): + """ + Gets the amino acid corresponding to either the residue name, 3-letter + abbreviation or 1-letter symbol. It is case-insensitive. + + Parameters + ---------- + key : str + 1-letter symbol, 3-letter abbreviation, or residue name + abbronly : bool + If True, only look for the 3-letter abbreviation (not the 1-letter + symbol) + + Returns + ------- + residue : :class:`AminoAcidResidue` + The residue corresponding to the given key + + Raises + ------ + KeyError if ``key`` is not a symbol, abbreviation, or case-insensitive + name of an amino acid residue, or any of its abbreviations. + """ + if len(key) == 1 and not abbronly: + return cls._all_residues_by_symbol[key.upper()] + if len(key) == 3: + return cls._all_residues_by_abbr[key.upper()] + # Handle C- and N-termini that may be prepended with C or N + if len(key) == 4 and key[0].upper() in 'CN': + return cls._all_residues_by_abbr[key[1:].upper()] + return cls._all_residues_by_name[key.upper()] + +ALA = AminoAcidResidue('Alanine', 'ALA', 'A') +ARG = AminoAcidResidue('Arginine', 'ARG', 'R') +ASN = AminoAcidResidue('Asparagine', 'ASN', 'N') +ASP = AminoAcidResidue('Aspartate' ,'ASP', 'D', ['ASH', 'AS4']) +CYS = AminoAcidResidue('Cysteine', 'CYS', 'C', ['CYM', 'CYX']) +GLU = AminoAcidResidue('Glutamate', 'GLU', 'E', ['GLH', 'GL4']) +GLN = AminoAcidResidue('Glutamine', 'GLN', 'Q') +GLY = AminoAcidResidue('Glycine', 'GLY', 'G') +HIS = AminoAcidResidue('Histidine', 'HIS', 'H', ['HIP', 'HIE', 'HID']) +HYP = AminoAcidResidue('Hydroxyproline', 'HYP', None) +ILE = AminoAcidResidue('Isoleucine', 'ILE', 'I') +LEU = AminoAcidResidue('Leucine', 'LEU', 'L') +LYS = AminoAcidResidue('Lysine', 'LYS', 'K', ['LYN']) +MET = AminoAcidResidue('Methionine', 'MET', 'M') +PHE = AminoAcidResidue('Phenylalanine', 'PHE', 'F') +PRO = AminoAcidResidue('Proline', 'PRO', 'P') +SER = AminoAcidResidue('Serine', 'SER', 'S') +THR = AminoAcidResidue('Threonine', 'THR', 'T') +TRP = AminoAcidResidue('Tryptophan', 'TRP', 'W') +TYR = AminoAcidResidue('Tyrosine', 'TYR', 'Y') +VAL = AminoAcidResidue('Valine', 'VAL', 'V') + +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +class DNAResidue(BiomolecularResidue): + """ An individual DNA residue + + Parameters + ---------- + name : str + The name of the residue + abbr : str + The abbreviation of the nucleic acid residue + aliases : list of str, optional + A list of other abbreviations that *also* refer to this residue + """ + _all_residues_by_name = dict() + _all_residues_by_abbr = dict() + _all_residues_by_symbol = dict() + all_residues = [] + + def __init__(self, name, abbr, aliases=None): + self.name = name + self.abbr = abbr + type(self)._all_residues_by_name[name.upper()] = self + type(self)._all_residues_by_abbr[abbr.upper()] = self + type(self).all_residues.append(self) + if aliases is not None: + for alias in aliases: + alias = alias.upper() + if alias in type(self)._all_residues_by_abbr: + raise ValueError('%s is already an abbreviation' % alias) + type(self)._all_residues_by_abbr[alias] = self + + def __repr__(self): + return '' % (self.name, self.abbr) + + @classmethod + def get(cls, key): + """ + Gets the nucleic acid corresponding to either the residue name or + abbreviation. It is case-insensitive. + + Parameters + ---------- + key : str + abbreviation or residue name + + Returns + ------- + residue : :class:`DNAResidue` + The residue corresponding to the given key + + Raises + ------ + KeyError if ``key`` is not a recognized residue name or abbreviation for + an DNA residue. + """ + try: + if key[-1] in '35': + return cls._all_residues_by_abbr[key[:-1].upper()] + return cls._all_residues_by_abbr[key.upper()] + except KeyError: + return cls._all_residues_by_name[key.upper()] + +class RNAResidue(DNAResidue): + """ An individual RNA residue + + Parameters + ---------- + name : str + The name of the residue + abbr : str + The abbreviation of the nucleic acid residue + aliases : list of str, optional + A list of other abbreviations that *also* refer to this residue + """ + _all_residues_by_name = dict() + _all_residues_by_abbr = dict() + _all_residues_by_symbol = dict() + all_residues = [] + + def __repr__(self): + return '' % (self.name, self.abbr) + + @classmethod + def get(cls, key): + """ + Gets the nucleic acid corresponding to either the residue name or + abbreviation. It is case-insensitive. + + Parameters + ---------- + key : str + abbreviation or residue name + + Returns + ------- + residue : :class:`RNAResidue` + The residue corresponding to the given key + + Raises + ------ + KeyError if ``key`` is not a recognized residue name or abbreviation for + an RNA residue. + """ + try: + if key[-1] in '35': + return cls._all_residues_by_abbr[key[:-1].upper()] + return cls._all_residues_by_abbr[key.upper()] + except KeyError: + return cls._all_residues_by_name[key.upper()] + +DG = DNAResidue('Guanine', 'DG', ['GUA', 'DG5', 'DG3', 'DGN']) +DC = DNAResidue('Cytosine', 'DC', ['CYT', 'DC5', 'DC3', 'DCN', 'DCP']) +DA = DNAResidue('Adenine', 'DA', ['ADE', 'DA5', 'DA3', 'DAN', 'DAP']) +DT = DNAResidue('Thymine', 'DT', ['THY', 'DT5', 'DT3']) +G = RNAResidue('Guanine', 'G', ['GUA', 'G5', 'G3', 'GN', 'RG', 'RG3', 'RG5', 'RGN']) +C = RNAResidue('Cytosine', 'C', ['CYT', 'CP', 'C5', 'C3', 'CN', 'RC', 'RC5', 'RC3', 'RCN']) +A = RNAResidue('Adenine', 'A', ['ADE', 'AP', 'A5', 'A3', 'AN', 'RA', 'RA3', 'RA5']) +U = RNAResidue('Uracil', 'U', ['URA', 'U3', 'U5', 'UN', 'RU', 'RU3', 'RU5', 'RUN']) +T = RNAResidue('Thymine', 'T', ['THY', 'T3', 'T5', 'TN', 'RT', 'RT3', 'RT5', 'RTN']) + +WATER_NAMES = {'WAT', 'HOH', 'TIP3', 'TIP4', 'TIP5', 'SPCE', 'SPC'} +SOLVENT_NAMES = WATER_NAMES | {'SOL'} +EXTRA_POINT_NAMES = {'EP', 'LP'} +CATION_NAMES = {'Na+', 'Li+', 'Mg+', 'Rb+', 'MG', 'Cs+', 'POT', 'SOD', 'MG2', + 'CAL', 'RUB', 'LIT', 'ZN2', 'CD2', 'NA', 'K+', 'K', 'NA+'} +ANION_NAMES = {'Cl-', 'Br-', 'F-', 'I-', 'CLA', 'CL', 'BR', 'CL-'} +ALLION_NAMES = CATION_NAMES | ANION_NAMES diff --git a/pmx/workflow/parmed/rosetta/__init__.py b/pmx/workflow/parmed/rosetta/__init__.py new file mode 100644 index 00000000..54e567b8 --- /dev/null +++ b/pmx/workflow/parmed/rosetta/__init__.py @@ -0,0 +1,9 @@ +""" +A package dealing with PyRosetta integration. +""" + +__author__ = "Carlos Xavier Hernandez " + +__all__ = ['RosettaPose'] + +from .pose import RosettaPose diff --git a/pmx/workflow/parmed/rosetta/pose.py b/pmx/workflow/parmed/rosetta/pose.py new file mode 100644 index 00000000..e7127419 --- /dev/null +++ b/pmx/workflow/parmed/rosetta/pose.py @@ -0,0 +1,84 @@ +""" +This package contains classes responsible for loading and dumping PyRosetta +Pose objects. +""" +from ..exceptions import RosettaError +from ..periodic_table import AtomicNum, Mass +from ..structure import Structure +from ..topologyobjects import Atom, ExtraPoint, Bond + +try: + from pyrosetta import Pose, AtomID +except ImportError: + Pose = AtomID = None + +def _n_prior(pose, nbr): + prior = -1 + for i in range(1, nbr.rsd()): + prior += pose.residue(i).natoms() + return prior + nbr.atomno() + +class RosettaPose: + + @staticmethod + def load(pose): + """ + Load a :class:`Pose` object and return a populated :class:`Structure` + instance + + Parameters + ---------- + pose : :class:`Pose` + PyRosetta :class:`Pose` object to convert + """ + if not Pose or not AtomID: + raise ImportError('Could not load the PyRosetta module.') + if not isinstance(pose, Pose): + raise TypeError('Object is not a PyRosetta Pose object.') + + struct = Structure() + + atnum = 1 + conf = pose.conformation() + for resid in range(1, pose.total_residue()+1): + res = pose.residue(resid) + resname = res.name3().strip() + chain = chr(res.chain()+ord('A')-1) + for atno, at in enumerate(res.atoms(), start=1): + try: + atinfo = res.atom_type(atno) + atname = res.atom_name(atno).strip() + if atinfo.is_virtual(): + atsym = 'EP' + else: + atsym = atinfo.element() + rmin = atinfo.lj_radius() + epsilon = atinfo.lj_wdepth() + atomic_number = AtomicNum[atsym] + mass = Mass[atsym] + except KeyError as err: + raise RosettaError(f'Could not recognize element: {atsym}') from err + + params = dict(atomic_number=atomic_number, name=atname, + charge=0.0, mass=mass, occupancy=0.0, + bfactor=0.0, altloc='', number=atnum, + rmin=rmin, epsilon=epsilon) + + if atinfo.is_virtual(): + atom = ExtraPoint(**params) + else: + atom = Atom(**params) + + atom.xx, atom.xy, atom.xz = (at.xyz()[0], at.xyz()[1], at.xyz()[2]) + + struct.add_atom(atom, resname, resid, chain, '') + atnum += 1 + try: + for nbr in conf.bonded_neighbor_all_res(AtomID(atno, resid)): + if nbr.rsd() < resid or (nbr.rsd() == resid and nbr.atomno() < atno): + struct.bonds.append(Bond(struct.atoms[_n_prior(pose, nbr)], atom)) + except Exception as err: + raise RosettaError('Could not add bonds.') from err + + struct.unchange() + return struct diff --git a/pmx/workflow/parmed/scripts.py b/pmx/workflow/parmed/scripts.py new file mode 100644 index 00000000..e09b2a6f --- /dev/null +++ b/pmx/workflow/parmed/scripts.py @@ -0,0 +1,200 @@ +from argparse import ArgumentParser +import datetime +import os +try: + import readline +except ImportError: + readline = None +import signal +import sys +import warnings +from . import load_file +from .exceptions import ParmedError +from .tools.logos import Logo +from .tools.exceptions import SeriousParmWarning, InterpreterError, ParmError +from .tools.parmed_cmd import ParmedCmd +from .tools.actions import Action +from .tools.parmlist import ParmList +from . import __version__ + +def clapp(): + # Set up new excepthook to clean up fatal exception printouts + def interrupted(*args, **kwargs): + """ Handle interruptions gracefully """ + sys.stdout.write('Interrupted\n') + sys.exit(1) + + signal.signal(signal.SIGINT, interrupted) + + # Define our own custom warning printer + def _print_warnings(message, category, filename, lineno, file=None, line=None): + """ Override the default showwarning method """ + file = sys.stderr if file is None else file + try: + file.write(f'{category.__name__}: {message}\n') + except IOError: + pass + + warnings.showwarning = _print_warnings + + # Set up parser + parser = ArgumentParser() + parser.add_argument('-v', '--version', action='version', version=f"%(prog)s: Version {__version__}") + group = parser.add_argument_group('Input Files') + group.add_argument('-i', '--input', dest='script', default=[], + metavar='FILE', help='''Script with ParmEd commands to execute. Default + reads from stdin. Can be specified multiple times to process multiple + input files.''', action='append') + group.add_argument('-p', '--parm', dest='prmtop', default=[], + metavar='', action='append', help='''List of topology files to load into + ParmEd. Can be specified multiple times to process multiple topologies.''') + group.add_argument('-c', '--inpcrd', dest='inpcrd', default=[], + metavar='', action='append', help='''List of inpcrd files to + load into ParmEd. They are paired with the topology files in the same + order that each set of files is specified on the command-line.''') + group = parser.add_argument_group('Output Files') + group.add_argument('-O', '--overwrite', dest='overwrite', default=False, + help='Allow ParmEd to overwrite existing files.', action='store_true') + group.add_argument('-l', '--logfile', dest='logfile', metavar='FILE', + default='parmed.log', help='''Log file with every command executed + during an interactive ParmEd session. Default is parmed.log''') + group = parser.add_argument_group('Interpreter Options', '''These options affect + how the ParmEd interpreter behaves in certain cases.''') + group.add_argument('--prompt', dest='prompt', default='>', metavar='PROMPT', + help='String to use as a command prompt.') + group.add_argument('-n', '--no-splash', dest='printlogo', action='store_false', + help='Prevent printing the greeting logo.', default=True) + group.add_argument('-e', '--enable-interpreter', dest='interpreter', + action='store_true', default=False, + help='''Allow arbitrary single Python commands or blocks of Python code + to be run. By default Python commands will not be run as a safeguard + for your system. Make sure you trust the source of the ParmEd command + before turning this option on.''') + group = parser.add_argument_group('Error Handling', '''These options control how + ParmEd handles various errors and warnings that appear occur during the + course of Action execution''') + group.add_argument('-s', '--strict', dest='strict', action='store_true', + default=True, help='''Prevent scripts from running past unrecognized input and actions + that end with an error. In interactive mode, actions with unrecognized inputs and + failed actions prevent any changes from being made to the topology, but does not quit + the interpreter. This is the default behavior.''') + group.add_argument('-r', '--relaxed', dest='strict', action='store_false', + help='''Scripts ignore unrecognized input and simply skip over failed + actions, executing the rest of the script. Unrecognized input in the + interactive interpreter emits a non-fatal warning.''') + parser.add_argument('prmtop_cl', nargs='?', metavar='', default=None, + help='Topology file to analyze.') + parser.add_argument('script_cl', nargs='?', metavar='