packmol-memgen-minimal 1.1.16__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- packmol_memgen/__init__.py +2 -0
- packmol_memgen/__version__.py +34 -0
- packmol_memgen/data/LICENSE.Apache-2.0 +201 -0
- packmol_memgen/data/extra_solvents.lib +789 -0
- packmol_memgen/data/frcmod.lipid_ext +97 -0
- packmol_memgen/data/frcmod.solvents +129 -0
- packmol_memgen/data/insane_lipids.txt +138 -0
- packmol_memgen/data/insane_solvents.txt +45 -0
- packmol_memgen/data/leaprc.extra_solvents +42 -0
- packmol_memgen/data/leaprc.lipid_ext +48 -0
- packmol_memgen/data/lipid_ext.lib +12312 -0
- packmol_memgen/data/martini_v3.0.0.itp +356605 -0
- packmol_memgen/data/memgen.parm +4082 -0
- packmol_memgen/data/pdbs.tar.gz +0 -0
- packmol_memgen/data/solvent.parm +14 -0
- packmol_memgen/example/example.sh +31 -0
- packmol_memgen/lib/__init__.py +0 -0
- packmol_memgen/lib/amber.py +77 -0
- packmol_memgen/lib/charmmlipid2amber/__init__.py +0 -0
- packmol_memgen/lib/charmmlipid2amber/charmmlipid2amber.csv +7164 -0
- packmol_memgen/lib/charmmlipid2amber/charmmlipid2amber.py +225 -0
- packmol_memgen/lib/pdbremix/LICENSE +21 -0
- packmol_memgen/lib/pdbremix/__init__.py +0 -0
- packmol_memgen/lib/pdbremix/_version.py +1 -0
- packmol_memgen/lib/pdbremix/amber.py +1103 -0
- packmol_memgen/lib/pdbremix/asa.py +227 -0
- packmol_memgen/lib/pdbremix/data/aminoacid.pdb +334 -0
- packmol_memgen/lib/pdbremix/data/binaries.json +26 -0
- packmol_memgen/lib/pdbremix/data/charmm22.parameter +2250 -0
- packmol_memgen/lib/pdbremix/data/charmm22.topology +1635 -0
- packmol_memgen/lib/pdbremix/data/color_b.py +682 -0
- packmol_memgen/lib/pdbremix/data/hin.lib +130 -0
- packmol_memgen/lib/pdbremix/data/hydroxide.lib +88 -0
- packmol_memgen/lib/pdbremix/data/make_chi.py +92 -0
- packmol_memgen/lib/pdbremix/data/opls.parameter +1108 -0
- packmol_memgen/lib/pdbremix/data/opls.topology +1869 -0
- packmol_memgen/lib/pdbremix/data/phd.frcmod +82 -0
- packmol_memgen/lib/pdbremix/data/phd.leaprc +4 -0
- packmol_memgen/lib/pdbremix/data/phd.prepin +35 -0
- packmol_memgen/lib/pdbremix/data/template.pdb +334 -0
- packmol_memgen/lib/pdbremix/data/znb.frcmod +24 -0
- packmol_memgen/lib/pdbremix/data/znb.leaprc +7 -0
- packmol_memgen/lib/pdbremix/data/znb.lib +69 -0
- packmol_memgen/lib/pdbremix/data.py +264 -0
- packmol_memgen/lib/pdbremix/fetch.py +102 -0
- packmol_memgen/lib/pdbremix/force.py +627 -0
- packmol_memgen/lib/pdbremix/gromacs.py +978 -0
- packmol_memgen/lib/pdbremix/lib/__init__.py +0 -0
- packmol_memgen/lib/pdbremix/lib/docopt.py +579 -0
- packmol_memgen/lib/pdbremix/lib/pyqcprot.py +305 -0
- packmol_memgen/lib/pdbremix/namd.py +1078 -0
- packmol_memgen/lib/pdbremix/pdbatoms.py +543 -0
- packmol_memgen/lib/pdbremix/pdbtext.py +120 -0
- packmol_memgen/lib/pdbremix/protein.py +311 -0
- packmol_memgen/lib/pdbremix/pymol.py +480 -0
- packmol_memgen/lib/pdbremix/rmsd.py +203 -0
- packmol_memgen/lib/pdbremix/simulate.py +420 -0
- packmol_memgen/lib/pdbremix/spacehash.py +73 -0
- packmol_memgen/lib/pdbremix/trajectory.py +286 -0
- packmol_memgen/lib/pdbremix/util.py +273 -0
- packmol_memgen/lib/pdbremix/v3.py +16 -0
- packmol_memgen/lib/pdbremix/v3array.py +482 -0
- packmol_memgen/lib/pdbremix/v3numpy.py +350 -0
- packmol_memgen/lib/pdbremix/volume.py +155 -0
- packmol_memgen/lib/utils.py +1017 -0
- packmol_memgen/main.py +2827 -0
- packmol_memgen_minimal-1.1.16.dist-info/METADATA +664 -0
- packmol_memgen_minimal-1.1.16.dist-info/RECORD +71 -0
- packmol_memgen_minimal-1.1.16.dist-info/WHEEL +4 -0
- packmol_memgen_minimal-1.1.16.dist-info/entry_points.txt +2 -0
- packmol_memgen_minimal-1.1.16.dist-info/licenses/LICENSE +338 -0
packmol_memgen/main.py
ADDED
|
@@ -0,0 +1,2827 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
from __future__ import print_function
|
|
4
|
+
import os, sys, math, subprocess, random, argparse, shutil, atexit, signal, logging, shlex, glob, importlib.util, re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import pandas as pd
|
|
7
|
+
import tarfile
|
|
8
|
+
script_path = os.path.abspath(os.path.dirname(__file__))+os.path.sep
|
|
9
|
+
sys.path.append(script_path)
|
|
10
|
+
from argparse import RawDescriptionHelpFormatter
|
|
11
|
+
from lib.utils import *
|
|
12
|
+
from lib.amber import *
|
|
13
|
+
import tqdm
|
|
14
|
+
from lib.pdbremix.lib.docopt import docopt
|
|
15
|
+
from lib.pdbremix import pdbatoms
|
|
16
|
+
from lib.pdbremix.rmsd import *
|
|
17
|
+
from lib.pdbremix.volume import volume
|
|
18
|
+
from lib.charmmlipid2amber.charmmlipid2amber import charmmlipid2amber
|
|
19
|
+
|
|
20
|
+
#CHECK CHANGELOG IN README.rst
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
os.environ['COLUMNS'] = str(shutil.get_terminal_size()[0])
|
|
24
|
+
except:
|
|
25
|
+
try:
|
|
26
|
+
from backports.shutil_get_terminal_size import get_terminal_size
|
|
27
|
+
os.environ['COLUMNS'] = str(get_terminal_size()[0])
|
|
28
|
+
except:
|
|
29
|
+
os.environ['COLUMNS'] = "80"
|
|
30
|
+
|
|
31
|
+
def _prepend_uv_tool_dirs_to_path():
|
|
32
|
+
# Ensure tools installed by `uv tool install` are discoverable via PATH.
|
|
33
|
+
tool_names = ("packmol_memgen_minimal", "packmol-memgen-minimal")
|
|
34
|
+
candidates = []
|
|
35
|
+
|
|
36
|
+
uv_tool_bin_dir = os.environ.get("UV_TOOL_BIN_DIR", "").strip()
|
|
37
|
+
if uv_tool_bin_dir:
|
|
38
|
+
candidates.append(Path(uv_tool_bin_dir))
|
|
39
|
+
|
|
40
|
+
uv_tool_dir = os.environ.get("UV_TOOL_DIR", "").strip()
|
|
41
|
+
if uv_tool_dir:
|
|
42
|
+
uv_tool_root = Path(uv_tool_dir)
|
|
43
|
+
candidates.append(uv_tool_root / "bin")
|
|
44
|
+
for name in tool_names:
|
|
45
|
+
candidates.append(uv_tool_root / "tools" / name / "bin")
|
|
46
|
+
|
|
47
|
+
default_root = Path.home() / ".local" / "share" / "uv"
|
|
48
|
+
candidates.append(default_root / "bin")
|
|
49
|
+
for name in tool_names:
|
|
50
|
+
candidates.append(default_root / "tools" / name / "bin")
|
|
51
|
+
|
|
52
|
+
current = os.environ.get("PATH", "")
|
|
53
|
+
parts = current.split(os.pathsep) if current else []
|
|
54
|
+
for path in candidates:
|
|
55
|
+
if path and path.exists():
|
|
56
|
+
path_str = str(path)
|
|
57
|
+
if path_str not in parts:
|
|
58
|
+
parts.insert(0, path_str)
|
|
59
|
+
os.environ["PATH"] = os.pathsep.join(parts)
|
|
60
|
+
|
|
61
|
+
_prepend_uv_tool_dirs_to_path()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
explanation = """The script creates an input file for PACKMOL for creating a bilayer system with a protein inserted in it. The input pdb file will be protonated and oriented by default using pdb2pqr and MemPrO; the user is encouraged to check the input and output files carefully! If the protein is preoriented, for example by using the PPM webserver from OPM (http://opm.phar.umich.edu/server.php), be sure to set the corresponding flag (--preoriented). In some cases the packed system might crash during the first MD step. Changes in the box boundaries or repacking with --random as an argument might help.
|
|
65
|
+
|
|
66
|
+
If you use this script, please cite the tools reported at the end of the run:
|
|
67
|
+
|
|
68
|
+
- [PACKMOL-Memgen] Schott-Verdugo, S.; Gohlke, H. PACKMOL-Memgen: A Simple-To-Use, Generalized Workflow for Membrane-Protein–Lipid-Bilayer System Building. J. Chem. Inf. Model. 2019, 59 (6), 2522–2528. https://doi.org/10.1021/acs.jcim.9b00269.
|
|
69
|
+
- [PACKMOL] Martínez, L.; Andrade, R.; Birgin, E. G.; Martínez, J. M. PACKMOL: A Package for Building Initial Configurations for Molecular Dynamics Simulations. J. Comput. Chem. 2009, 30 (13), 2157–2164. https://doi.org/10.1002/jcc.21224.
|
|
70
|
+
- [MemPrO] Parrag, M.; Stansfeld, P. J. MemPrO: A Predictive Tool for Membrane Protein Orientation. J. Chem. Theory Comput. 2025. https://doi.org/10.1021/acs.jctc.5c01433.
|
|
71
|
+
- [Martini] Souza, P. C. T.; Alessandri, R.; Barnoud, J.; et al. Martini 3: a general purpose force field for coarse-grained molecular dynamics. Nat Methods 2021, 18, 382–388. https://doi.org/10.1038/s41592-021-01098-3.
|
|
72
|
+
- [PDB2PQR] Dolinsky, T. J.; Czodrowski, P.; Li, H.; Nielsen, J. E.; Jensen, J. H.; Klebe, G.; Baker, N. A. PDB2PQR: Expanding and Upgrading Automated Preparation of Biomolecular Structures for Molecular Simulations. Nucleic Acids Res. 2007, 35 (Web Server issue), W522–W525. https://doi.org/10.1093/nar/gkm276.
|
|
73
|
+
- [PDB2PQR] Dolinsky, T. J.; Nielsen, J. E.; McCammon, J. A.; Baker, N. A. PDB2PQR: An Automated Pipeline for the Setup of Poisson–Boltzmann Electrostatics Calculations. Nucleic Acids Res. 2004, 32 (Web Server issue), W665–W667. https://doi.org/10.1093/nar/gkh381.
|
|
74
|
+
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
explanation = explanation+"-"*int(os.environ['COLUMNS'])
|
|
78
|
+
|
|
79
|
+
short_help = "-h" in sys.argv
|
|
80
|
+
|
|
81
|
+
parser = argparse.ArgumentParser(prog="packmol-memgen", description = explanation, add_help=False, formatter_class=RawDescriptionHelpFormatter)
|
|
82
|
+
parser.add_argument("-h", action="help", help="prints this help message and exits" if short_help else "prints a short help message and exits")
|
|
83
|
+
parser.add_argument("--help", action="help", help="prints an extended help message and exits" if short_help else "prints this help message and exits")
|
|
84
|
+
parser.add_argument("--available_lipids",action="store_true", help="list of available lipids and corresponding charges")
|
|
85
|
+
parser.add_argument("--available_lipids_all",action="store_true", help="list all lipids including experimental. Huge output (>4000 lines)! Think about grepping the lipid you want (packmol-memgen --available_lipids_all | grep YOUR_LIPID_PATTERN)")
|
|
86
|
+
parser.add_argument("--available_solvents",action="store_true", help="list of available solvents and corresponding charges")
|
|
87
|
+
parser.add_argument("--available_ions",action="store_true", help="list of available ions and corresponding charges")
|
|
88
|
+
parser.add_argument("-l","--lipids",action="append",metavar="LIP1:LIP2//LIP3",help="Lipid(s) to be used for embeding the protein. It should be a single string separated by ':' . If different composition is used in leaflets, add '//' as a separator.[ex. CHL1:DOPC//DOPE for a lower leaflet with CHL1+DOPC and an upper leaflet with DOPE]. Can be defined multiple times for multi-bilayer systems (stacks 'up' or 'outside')")
|
|
89
|
+
parser.add_argument("-r","--ratio",action="append",metavar="R1:R2//R3", help="mixture ratio (set to 1 if only one lipid required). Must be in the same order and syntax as in lipids, and defined once per bilayer [ex. 1:2//1] ")
|
|
90
|
+
parser.add_argument("--solvents",type=str, metavar="SOL1:SOL2",help="Solvent(s) to be used for the packing. As lipids, it should be a single string separated by ':'. Water by default.")
|
|
91
|
+
parser.add_argument("--solvent_ratio",type=str,metavar="SR1:SR2", help="mixture ratio (set to 1 if only one solvent required). Must be in the same order and syntax as in solvents")
|
|
92
|
+
parser.add_argument("--dist", type=float, default=15.0, help=argparse.SUPPRESS if short_help else "specify the minimum distance between the maxmin values for x y and z to the box boundaries. Default = 15 A. Worst case scenario is considered, so real distance could be larger")
|
|
93
|
+
parser.add_argument("--dist_wat", type=float, default=17.5, help=argparse.SUPPRESS if short_help else "specify the width of the water layer over the membrane or protein in the z axis. Default = 17.5")
|
|
94
|
+
parser.add_argument("--distxy_fix", type=float, help="specify the membrane patch side length in the x and y axis. Useful when only lipids are packed! By default is calculated flexibly depending on the protein")
|
|
95
|
+
parser.add_argument("--watorient", action="store_true", help=argparse.SUPPRESS if short_help else "use 1.5 radius in packmol for water oxygen, to foster water orientation. Might help for packing particularly big water systems")
|
|
96
|
+
parser.add_argument("--noxy_cen", action="store_false", help=argparse.SUPPRESS if short_help else "disable default centering in the xy plane that ensures symmetric membrane building. Not recommended!")
|
|
97
|
+
parser.add_argument("--channel_plug", type=float, help=argparse.SUPPRESS if short_help else "establishes a cylindrical restraint on the lipids using the protein z height and the input value as xy radius. A value of 0 will use half of the protein radius. By default, no restraint is imposed.")
|
|
98
|
+
parser.add_argument("--self_assembly", action="store_true", help=argparse.SUPPRESS if short_help else "places lipids all over the packed box, and not in a bilayer.")
|
|
99
|
+
parser.add_argument("--xygauss",nargs=3,metavar=("C","D","H"), help=argparse.SUPPRESS if short_help else "set parameters for a curved 2d gaussian in the xy plane. Parameters are uncertainty in x, uncertainty in y and gaussian height. By default, membranes are flat.")
|
|
100
|
+
parser.add_argument("--curvature", type=float, default=None, help=argparse.SUPPRESS if short_help else "set the curvature of the membrane patch. By default, membranes are flat.")
|
|
101
|
+
parser.add_argument("--curv_radius", type=float, default=None, help=argparse.SUPPRESS if short_help else "inverse of curvature. Set the curvature as if on a vesicle with the provided radius.")
|
|
102
|
+
parser.add_argument("--dims", nargs=3,metavar=("X","Y","Z"), type=float,default=[0,0,0], help=argparse.SUPPRESS if short_help else "box dimensions vector for the x y z axes. Be sure to use dimensions that cover the complete protein to be packed!!")
|
|
103
|
+
parser.add_argument("--solvate", action="store_true", help=argparse.SUPPRESS if short_help else "solvate the system without adding lipids. Disables the flag --dist_wat, using only --dist to set the box size. Under development!")
|
|
104
|
+
parser.add_argument("--pbc", action="store_true", help=argparse.SUPPRESS if short_help else "use PBC option in packmol, and adapt constraints accordingly.")
|
|
105
|
+
parser.add_argument("--cubic", action="store_true", help=argparse.SUPPRESS if short_help else "cube shaped box. Only works with --solvate")
|
|
106
|
+
parser.add_argument("--vol", action="store_true", help=argparse.SUPPRESS if short_help else "do the lipid number estimation based on the volume occupied by the leaflet instead of APL. This might cause a great overestimation of the number of lipid molecules!")
|
|
107
|
+
parser.add_argument("--leaflet", type=float, default=23.0, help=argparse.SUPPRESS if short_help else "set desired leaflet width. 23 by default.")
|
|
108
|
+
parser.add_argument("--lip_offset", type=float, default=1.0, help=argparse.SUPPRESS if short_help else "factor that multiplies the x/y sizes for the lipid membrane segment. Might improve packing and handling by AMBER")
|
|
109
|
+
parser.add_argument("--apl_offset", action="append", help=argparse.SUPPRESS if short_help else "factor that multiplies the default APL values. Helpful if packing stretched membranes.")
|
|
110
|
+
parser.add_argument("--tailplane", type=float, help=argparse.SUPPRESS if short_help else "sets the position BELOW which the CH3 carbon atoms in the tail should be. By default defined in parameter file")
|
|
111
|
+
parser.add_argument("--headplane", type=float, help=argparse.SUPPRESS if short_help else "sets the position ABOVE which the PO4 phosphorus and N atoms in the polar head group should be.By default defined in parameter file")
|
|
112
|
+
parser.add_argument("--plot", action="store_true", help=argparse.SUPPRESS if short_help else "makes a simple plot of loop number vs GENCAN optimization function value, and outputs the values to GENCAN.dat")
|
|
113
|
+
parser.add_argument("--traj", action="store_true", help=argparse.SUPPRESS if short_help else "saves all intermediate steps into separate pdb files")
|
|
114
|
+
parser.add_argument("--notgridvol", action="store_false", help=argparse.SUPPRESS if short_help else "skips grid building for volume estimation, and the calculation is done just by estimating density")
|
|
115
|
+
parser.add_argument("--keep", action="store_true", help=argparse.SUPPRESS if short_help else "skips deleting temporary files")
|
|
116
|
+
parser.add_argument("--noprogress", action="store_true", help=argparse.SUPPRESS if short_help else "avoids the printing of progress bar with time estimation in the final stage. Recommended if the job is piped into a file")
|
|
117
|
+
parser.add_argument("--apl_exp", action="store_true", help=argparse.SUPPRESS if short_help else "use experimental APL where available, like AmberTools18 release. Kept for consistency with older versions. By default, terms estimated with Lipid17 are used")
|
|
118
|
+
parser.add_argument("--memgen_parm", type=str, help=argparse.SUPPRESS if short_help else "load custom memgen.parm file with APL and VOL values. Extends and overwrites default values")
|
|
119
|
+
parser.add_argument("--solvent_parm", type=str, help=argparse.SUPPRESS if short_help else "load custom solvent.parm file with densities and molecular weights. Extends and overwrites default values")
|
|
120
|
+
parser.add_argument("--overwrite", action="store_true", help=argparse.SUPPRESS if short_help else "overwrite, even if files are present")
|
|
121
|
+
parser.add_argument("--log",type=str,default="packmol-memgen.log",help=argparse.SUPPRESS if short_help else "log file name where detailed information is to be written")
|
|
122
|
+
parser.add_argument("-o","--output",type=str, help=argparse.SUPPRESS if short_help else "name of the PACKMOL generated PDB file")
|
|
123
|
+
parser.add_argument("--outdir",type=str,default=".", help=argparse.SUPPRESS if short_help else "output directory for all generated files (logs, packmol scripts, intermediate structures, final output)")
|
|
124
|
+
parser.add_argument("--charmm", action="store_true", help=argparse.SUPPRESS if short_help else "the output will be in CHARMM format instead of AMBER. Works only for small subset of lipids (see --available_lipids)")
|
|
125
|
+
parser.add_argument("--translate", nargs=3, type=float, default=[0,0,0], help=argparse.SUPPRESS if short_help else "pass a vector as x y z to translate the oriented pdb. Ex. ' 0 0 4 '")
|
|
126
|
+
parser.add_argument("--sirah", action="store_true", help=argparse.SUPPRESS if short_help else "use SIRAH lipids, and corase-grain protein input. Will adapt tolerance accordingly. Only small subset of lipids available!")
|
|
127
|
+
parser.add_argument("--martini", action="store_true", help=argparse.SUPPRESS if short_help else "enable MemPrO+Insane4MemPrO system building for CG inputs")
|
|
128
|
+
parser.add_argument("--martinized", action="store_true", help=argparse.SUPPRESS if short_help else "skip martinize2 coarse-graining (input already Martini CG)")
|
|
129
|
+
parser.add_argument("--verbose", action="store_true", help=argparse.SUPPRESS if short_help else "verbose mode")
|
|
130
|
+
parser.add_argument("--xponge", action="store_true", help=argparse.SUPPRESS if short_help else "postprocess ion names to Xponge-compatible identifiers")
|
|
131
|
+
|
|
132
|
+
parser.add_argument("--pdb2pqr", action="store_true", help=argparse.SUPPRESS if short_help else "uses pdb2pqr to protonate the protein structure")
|
|
133
|
+
parser.add_argument("--pdb2pqr_pH", type=float, default=7.0, help=argparse.SUPPRESS if short_help else "pH to be used by pdb2pqr to protonate the structure")
|
|
134
|
+
parser.add_argument("--notprotonate", action="store_false", help=argparse.SUPPRESS if short_help else "skips protonation")
|
|
135
|
+
|
|
136
|
+
inputs = parser.add_argument_group('Inputs')
|
|
137
|
+
inputs.add_argument("-p","--pdb", action="append", help="PDB or PQR file(s) to embed. If many bilayers, it has to be specified once for each bilayer. 'None' can be specified and a bilayer without protein will be generated [ex. --pdb PDB1.pdb --pdb None --pdb PDB2.pdb (3 bilayers without protein in the middle)]. If no PDB is provided, the bilayer(s) will be membrane only (--distxy_fix has to be defined).")
|
|
138
|
+
inputs.add_argument("--solute", action="append", help=argparse.SUPPRESS if short_help else "adds pdb as solute into the water. Concentration has to be specified")
|
|
139
|
+
inputs.add_argument("--solute_con", action="append", help=argparse.SUPPRESS if short_help else "number of molecules/concentration to be used. Concentrations are specified in Molar by adding an 'M' as a suffix (Ex. 0.15M). If not added, a number of molecules is assumed.")
|
|
140
|
+
inputs.add_argument("--solute_charge", action="append", help=argparse.SUPPRESS if short_help else "absolute charge of the included solute (Ex. -2). To be considered in the system neutralization")
|
|
141
|
+
inputs.add_argument("--solute_inmem", action="store_true", help=argparse.SUPPRESS if short_help else "solute should be added to membrane fraction")
|
|
142
|
+
inputs.add_argument("--solute_prot_dist", type=float, help=argparse.SUPPRESS if short_help else "establishes a cylindrical restraint using the protein xy radius and z height + the input value. A value of 0 will use the protein radius. By default, no restraint is imposed.")
|
|
143
|
+
|
|
144
|
+
embedopt = parser.add_argument_group('MemPrO options')
|
|
145
|
+
embedopt.add_argument("--preoriented", action="store_true", help="use this flag if the protein has been previosuly oriented and you want to avoid running MemPrO (i.e. from OPM)")
|
|
146
|
+
embedopt.add_argument("--double_span", action="store_true", help=argparse.SUPPRESS) #"orient protein twice, assuming it spans two membrane bilayer")
|
|
147
|
+
embedopt.add_argument("--n_ter", action="append", help=argparse.SUPPRESS if short_help else "'in' or 'out'. By default proteins are oriented with the n_ter oriented 'in' (or 'down'). relevant for multi layer system. If defined for one protein, it has to be defined for all of them, following previous order")
|
|
148
|
+
embedopt.add_argument("--keepligs", action="store_true", help=argparse.SUPPRESS if short_help else "MemPrO ignores HETATM records; use with care if you rely on ligands")
|
|
149
|
+
embedopt.add_argument("--mempro",type=str, help=argparse.SUPPRESS if short_help else "Path to MemPrO executable or MemPrO_Script.py")
|
|
150
|
+
embedopt.add_argument("--mempro_grid",type=int,default=36, help=argparse.SUPPRESS if short_help else "MemPrO grid size (-ng)")
|
|
151
|
+
embedopt.add_argument("--mempro_iters",type=int,default=150, help=argparse.SUPPRESS if short_help else "MemPrO minimization iterations (-ni)")
|
|
152
|
+
embedopt.add_argument("--mempro_rank",type=str,default="auto",choices=["auto","h","p"],help=argparse.SUPPRESS if short_help else "MemPrO rank mode (-rank)")
|
|
153
|
+
embedopt.add_argument("--mempro_args",type=str, help=argparse.SUPPRESS if short_help else "Extra arguments passed to MemPrO")
|
|
154
|
+
embedopt.add_argument("--mempro_curvature", action="store_true", help=argparse.SUPPRESS if short_help else "use MemPrO curvature (-c) and set --curvature from Global curvature in info_rank_1.txt")
|
|
155
|
+
embedopt.add_argument("--no-keep-mempro", action="store_false", dest="keep_mempro", help=argparse.SUPPRESS if short_help else "remove MemPrO outputs and working folder during cleanup")
|
|
156
|
+
embedopt.add_argument("--insane_build_ranks",type=int, dest="build_system", help=argparse.SUPPRESS if short_help else "Build a MD ready CG-system for ranks < n via MemPrO and Insane4MemPrO.")
|
|
157
|
+
embedopt.add_argument("--insane_args",type=str, dest="build_arguments", help=argparse.SUPPRESS if short_help else "Arguments passed to Insane4MemPrO via MemPrO (-bd_args)")
|
|
158
|
+
|
|
159
|
+
packmolopt = parser.add_argument_group('PACKMOL options')
|
|
160
|
+
packmolopt.add_argument("--nloop", type=int,default=20, help=argparse.SUPPRESS if short_help else "number of nloops for GENCAN routine in PACKMOL. PACKMOL MEMGEN uses 20 by default; you might consider increasing the number to improve packing. Increasing the number of components requires more GENCAN loops.")
|
|
161
|
+
packmolopt.add_argument("--nloop_all", type=int,default=100, help=argparse.SUPPRESS if short_help else "number of nloops for all-together packing. PACKMOL MEMGEN uses 100 by default.")
|
|
162
|
+
packmolopt.add_argument("--tolerance", type=float,default=2.0, help=argparse.SUPPRESS if short_help else "tolerance for detecting clashes between molecules in PACKMOL (defined as radius1+radius2). PACKMOL uses 2.0 by default.")
|
|
163
|
+
packmolopt.add_argument("--prot_rad", type=float,default=1.5, help=argparse.SUPPRESS if short_help else "radius considered for protein atoms to establish the tolerance for detecting clashes. PACKMOL MEMGEN uses 1.5 by default.")
|
|
164
|
+
packmolopt.add_argument("--writeout", help=argparse.SUPPRESS if short_help else "frequency for writing intermediate results. PACKMOL uses 10 by default.")
|
|
165
|
+
packmolopt.add_argument("--notrun", action="store_true", help=argparse.SUPPRESS if short_help else "will not run PACKMOL, even if it's available")
|
|
166
|
+
packmolopt.add_argument("--random", action="store_true", help=argparse.SUPPRESS if short_help else "turns PACKMOL random seed generator on. If a previous packing failed in the minimization problem, repacking with this feature on might solve the problem.")
|
|
167
|
+
packmolopt.add_argument("--packall", action="store_true", help=argparse.SUPPRESS if short_help else "skips initial individual packing steps")
|
|
168
|
+
packmolopt.add_argument("--short_penalty", action="store_true", help=argparse.SUPPRESS if short_help else "add a short range penalty for heavily overlapping atoms with default PACKMOL options")
|
|
169
|
+
packmolopt.add_argument("--movebadrandom", action="store_true", help=argparse.SUPPRESS if short_help else "randomizes positions of badly placed molecules in initial guess")
|
|
170
|
+
packmolopt.add_argument("--maxit", type=int,default=20, help=argparse.SUPPRESS if short_help else "number of GENCAN iterations per loop. 20 by default.")
|
|
171
|
+
packmolopt.add_argument("--movefrac", type=float,default=0.05, help=argparse.SUPPRESS if short_help else "fraction of molecules to be moved. 0.05 by default.")
|
|
172
|
+
packmolopt.add_argument("--packlog",type=str,default="packmol", help=argparse.SUPPRESS if short_help else "prefix for generated PACKMOL input and log files")
|
|
173
|
+
packmolopt.add_argument("--packmol",type=str, help=argparse.SUPPRESS)
|
|
174
|
+
packmolopt.add_argument("--hexadecimal_indices", action="store_true", default=True, help=argparse.SUPPRESS if short_help else "use PACKMOL hexadecimal_indices output; will be converted to hybrid-36 in final PDB")
|
|
175
|
+
packmolopt.add_argument("--no-hexadecimal-indices", action="store_false", dest="hexadecimal_indices", help=argparse.SUPPRESS if short_help else "disable PACKMOL hexadecimal_indices output")
|
|
176
|
+
|
|
177
|
+
saltopt = parser.add_argument_group('Salts and charges')
|
|
178
|
+
saltopt.add_argument("--salt", action="store_true", help=argparse.SUPPRESS if short_help else "adds salt at a concentration of 0.15M by default. Salt is always added considering estimated charges for the system.")
|
|
179
|
+
saltopt.add_argument("--salt_c",default="K+", help=argparse.SUPPRESS if short_help else "cation to add. (K+ by default)")
|
|
180
|
+
saltopt.add_argument("--salt_a",default="Cl-", help=argparse.SUPPRESS if short_help else "anion to add. (Cl- by default)")
|
|
181
|
+
saltopt.add_argument("--saltcon", type=float, default=0.15, help=argparse.SUPPRESS if short_help else "modifies the default concentration for KCl. [M]")
|
|
182
|
+
saltopt.add_argument("--salt_override",action="store_true", help=argparse.SUPPRESS if short_help else "if the concentration of salt specified is less than the required to neutralize, will try to continue omitting the warning")
|
|
183
|
+
saltopt.add_argument("--nocounter",action="store_true", help=argparse.SUPPRESS if short_help else "no counterions are added. System charge is handled by downstream tools")
|
|
184
|
+
saltopt.add_argument("--charge_pdb_delta", action="append", help=argparse.SUPPRESS if short_help else "add a given formal charge value per pdb. Might be useful to compensate for charges of non-standard residues not accounted by the script. If many pdbs, it has to be specified once for each pdb.")
|
|
185
|
+
|
|
186
|
+
compel = parser.add_argument_group('Computational electophysiology')
|
|
187
|
+
compel.add_argument("--double", action="store_true", help=argparse.SUPPRESS if short_help else "asumes a stacked double bilayer system for CompEL. The composition in --lipids will be used for both bilayers flipping the leaflets")
|
|
188
|
+
compel.add_argument("--charge_imbalance", type=int, default=0, help=argparse.SUPPRESS if short_help else "sets a charge imbalance between compartments (in electron charge units). A positive imbalance implies an increase (decrease) in cations (anions) in the central compartment.")
|
|
189
|
+
compel.add_argument("--imbalance_ion", type=str, default="cat", choices=["cat","an"], help=argparse.SUPPRESS if short_help else "sets if cations or anions are used to imbalance the system charges. ('cat' by default)")
|
|
190
|
+
|
|
191
|
+
logger = logging.getLogger("pmmg_log")
|
|
192
|
+
|
|
193
|
+
class PACKMOLMemgen(object):
|
|
194
|
+
"""
|
|
195
|
+
Class that manages PACKMOL-Memgen arguments and initialization
|
|
196
|
+
"""
|
|
197
|
+
def __init__(self, args=None):
|
|
198
|
+
logger = logging.getLogger("pmmg_log")
|
|
199
|
+
if args==None:
|
|
200
|
+
args = parser.parse_args()
|
|
201
|
+
|
|
202
|
+
#Get args in self
|
|
203
|
+
for key, value in args.__dict__.items():
|
|
204
|
+
setattr(self, key, value)
|
|
205
|
+
self.outdir = os.path.abspath(getattr(self, "outdir", ".") or ".")
|
|
206
|
+
self._used_tools = {"packmol-memgen"}
|
|
207
|
+
self.martini_build_output = None
|
|
208
|
+
|
|
209
|
+
def _out_path(self, *parts):
|
|
210
|
+
return os.path.join(self.outdir, *parts)
|
|
211
|
+
|
|
212
|
+
def _packmol_path(self, path):
|
|
213
|
+
if any(c.isspace() for c in path):
|
|
214
|
+
return f"\"{path}\""
|
|
215
|
+
return path
|
|
216
|
+
|
|
217
|
+
def prepare(self):
|
|
218
|
+
|
|
219
|
+
if self.verbose:
|
|
220
|
+
logger.setLevel(logging.DEBUG)
|
|
221
|
+
else:
|
|
222
|
+
print("--verbose for details of the packing process", file=sys.stderr)
|
|
223
|
+
logger.debug("Execution line: "+" ".join(sys.argv))
|
|
224
|
+
os.makedirs(self.outdir, exist_ok=True)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
#Path to local Packmol installation (http://www.ime.unicamp.br/~martinez/packmol/home.shtml)
|
|
228
|
+
rep = "data"
|
|
229
|
+
lib = "lib"
|
|
230
|
+
self.run = not self.notrun
|
|
231
|
+
if self.apl_exp:
|
|
232
|
+
apl = "APL_EXP"
|
|
233
|
+
else:
|
|
234
|
+
apl = "APL_FF"
|
|
235
|
+
packmol_inc = os.path.join(script_path, lib, "packmol", "packmol" + exe_suffix)
|
|
236
|
+
self.packmol_cmd = None
|
|
237
|
+
packmol_path = None
|
|
238
|
+
if self.packmol is None:
|
|
239
|
+
packmol_env = shutil.which("packmol" + exe_suffix) or shutil.which("packmol")
|
|
240
|
+
|
|
241
|
+
if os.path.exists(packmol_inc):
|
|
242
|
+
packmol_path = packmol_inc
|
|
243
|
+
elif packmol_env:
|
|
244
|
+
packmol_path = packmol_env
|
|
245
|
+
else:
|
|
246
|
+
packmol_path = None
|
|
247
|
+
else:
|
|
248
|
+
if self.packmol:
|
|
249
|
+
packmol_path = self.packmol
|
|
250
|
+
|
|
251
|
+
if packmol_path:
|
|
252
|
+
if os.path.exists(packmol_path):
|
|
253
|
+
self.packmol_cmd = [packmol_path]
|
|
254
|
+
else:
|
|
255
|
+
self.packmol_cmd = shlex.split(packmol_path)
|
|
256
|
+
else:
|
|
257
|
+
self.packmol_cmd = None
|
|
258
|
+
|
|
259
|
+
if not self.packmol_cmd:
|
|
260
|
+
miss = "Packmol not found. Execution will only create the packmol input script."
|
|
261
|
+
logger.info("\n"+len(miss)*"#"+"\n"+miss+"\n"+len(miss)*"#"+"\n")
|
|
262
|
+
self.run = False
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
############## PARSE PARAMS FROM REP #########
|
|
266
|
+
|
|
267
|
+
pdbtar = tarfile.open(os.path.join(script_path, rep,"pdbs.tar.gz"),"r:gz")
|
|
268
|
+
parms = open(os.path.join(script_path, rep, "memgen.parm"), "r")
|
|
269
|
+
parmlines = parms.readlines()
|
|
270
|
+
parms.close()
|
|
271
|
+
if self.memgen_parm is not None:
|
|
272
|
+
logger.debug("Loading custom memgen.parm: "+self.memgen_parm)
|
|
273
|
+
logger.debug("Parameters will be extended or overwritten over default values")
|
|
274
|
+
parms = open(self.memgen_parm, "r")
|
|
275
|
+
parmlines += parms.readlines()
|
|
276
|
+
parms.close()
|
|
277
|
+
self.parameters = {}
|
|
278
|
+
ext_par = False
|
|
279
|
+
for line in parmlines:
|
|
280
|
+
if line[0] == "#":
|
|
281
|
+
if line.startswith("#EXTENDED LIPIDS"):
|
|
282
|
+
ext_par = True
|
|
283
|
+
continue
|
|
284
|
+
else:
|
|
285
|
+
values = line.split()
|
|
286
|
+
self.parameters[values[0]] = {}
|
|
287
|
+
self.parameters[values[0]].update({
|
|
288
|
+
"p_atm":values[1],
|
|
289
|
+
"t_atm":values[2],
|
|
290
|
+
"APL_FF":values[3],
|
|
291
|
+
"APL_EXP":values[4],
|
|
292
|
+
"V":values[5],
|
|
293
|
+
"charge":values[6],
|
|
294
|
+
"h_bound":values[7],
|
|
295
|
+
"t_bound":values[8],
|
|
296
|
+
"C":values[9],
|
|
297
|
+
"VH":values[10],
|
|
298
|
+
"charmm":values[11],
|
|
299
|
+
"name":values[12],
|
|
300
|
+
"ext":ext_par
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
if self.available_lipids or self.available_lipids_all:
|
|
304
|
+
if self.memgen_parm is not None:
|
|
305
|
+
logger.debug("List will include parameters in the parsed parameter file!: "+self.memgen_parm)
|
|
306
|
+
if self.available_lipids:
|
|
307
|
+
pformat = "{:<9}{:>6}{:>70}"
|
|
308
|
+
else:
|
|
309
|
+
pformat = "{:<9}{:>6}{:>130}"
|
|
310
|
+
table_header= (pformat+"{:>15}").format("Lipid","Charge","Full-name","Comment")
|
|
311
|
+
print("\n"+table_header, file=sys.stderr)
|
|
312
|
+
print("-"*len(table_header), file=sys.stderr)
|
|
313
|
+
for key in sorted(self.parameters):
|
|
314
|
+
if not self.available_lipids_all and self.parameters[key]["ext"]:
|
|
315
|
+
continue
|
|
316
|
+
elif is_number(key[0]) or key[-2:]=="CL" or "PI" in key or self.parameters[key]["ext"]:
|
|
317
|
+
print((pformat+" ***Parameters from PACKMOL-Memgen Lipid_ext").format(key,self.parameters[key]["charge"],self.parameters[key]["name"]))
|
|
318
|
+
elif key[-2:] == "SM":
|
|
319
|
+
print((pformat+" ***Only available with Lipid21").format(key,self.parameters[key]["charge"],self.parameters[key]["name"]))
|
|
320
|
+
elif key.startswith("si"):
|
|
321
|
+
print((pformat+" ***Only available with SIRAH").format(key,self.parameters[key]["charge"],self.parameters[key]["name"]))
|
|
322
|
+
else:
|
|
323
|
+
print(pformat.format(key,self.parameters[key]["charge"],self.parameters[key]["name"]))
|
|
324
|
+
insane_lipids = self._load_insane_lipids()
|
|
325
|
+
if insane_lipids:
|
|
326
|
+
print("\nInsane4MemPrO lipids (lipidsa):", file=sys.stderr)
|
|
327
|
+
for name in sorted(insane_lipids):
|
|
328
|
+
print(name, file=sys.stderr)
|
|
329
|
+
exit()
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
solvent_parms = open(os.path.join(script_path, rep, "solvent.parm"), "r")
|
|
333
|
+
sparmlines = solvent_parms.readlines()
|
|
334
|
+
solvent_parms.close()
|
|
335
|
+
if self.solvent_parm is not None:
|
|
336
|
+
logger.debug("Loading custom solvent.parm: "+self.solvent_parm)
|
|
337
|
+
solvent_parms = open(self.solvent_parm, "r")
|
|
338
|
+
sparmlines += solvent_parms.readlines()
|
|
339
|
+
solvent_parms.close()
|
|
340
|
+
self.sparameters = {}
|
|
341
|
+
for line in sparmlines:
|
|
342
|
+
if line[0] == "#":
|
|
343
|
+
continue
|
|
344
|
+
values = line.split()
|
|
345
|
+
self.sparameters[values[0]] = {}
|
|
346
|
+
self.sparameters[values[0]].update({
|
|
347
|
+
"density":values[1],
|
|
348
|
+
"MW":values[2],
|
|
349
|
+
"charge":values[3],
|
|
350
|
+
"name":values[4],
|
|
351
|
+
"source":values[5].replace("_"," ") if len(values) == 6 else "-"
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
if self.available_solvents:
|
|
355
|
+
pformat = "{:<9}{:>15}{:>15}{:>15}{:>40}{:>80}"
|
|
356
|
+
table_header= pformat.format("Solvent","Density [g/cm3]","MW [g/mol]","Charge","Full-name","Comments")
|
|
357
|
+
print("\n"+table_header, file=sys.stderr)
|
|
358
|
+
print("-"*len(table_header), file=sys.stderr)
|
|
359
|
+
for key in sorted(self.sparameters):
|
|
360
|
+
print(pformat.format(key,self.sparameters[key]["density"],self.sparameters[key]["MW"],self.sparameters[key]["charge"],self.sparameters[key]["name"],self.sparameters[key]["source"]))
|
|
361
|
+
insane_solvents = self._load_insane_solvents()
|
|
362
|
+
if insane_solvents:
|
|
363
|
+
print("\nInsane4MemPrO solvents (solventParticles):", file=sys.stderr)
|
|
364
|
+
for name in sorted(insane_solvents):
|
|
365
|
+
print(name, file=sys.stderr)
|
|
366
|
+
exit()
|
|
367
|
+
|
|
368
|
+
if self.available_ions:
|
|
369
|
+
pformat = "{:<9}{:>15}"
|
|
370
|
+
table_header= pformat.format("Ion","Charge")
|
|
371
|
+
print("\n"+table_header, file=sys.stderr)
|
|
372
|
+
print("-"*len(table_header), file=sys.stderr)
|
|
373
|
+
for key in sorted(amber_ion_dict):
|
|
374
|
+
print(pformat.format(key,amber_ion_dict[key][1]))
|
|
375
|
+
exit()
|
|
376
|
+
|
|
377
|
+
############ ARGPARSE ARGUMENTS ###################
|
|
378
|
+
|
|
379
|
+
if self.martini:
|
|
380
|
+
self._warn_martini_unsupported()
|
|
381
|
+
|
|
382
|
+
if self.curv_radius is not None:
|
|
383
|
+
self.curvature = 1/self.curv_radius
|
|
384
|
+
|
|
385
|
+
if self.curvature is not None:
|
|
386
|
+
sphere_radius = 1/self.curvature
|
|
387
|
+
if self.vol:
|
|
388
|
+
logger.critical("CRITICAL:\n --vol and curvature are not compatible!")
|
|
389
|
+
exit()
|
|
390
|
+
if self.solvate:
|
|
391
|
+
logger.critical("CRITICAL:\n --solvate and curvature are not compatible!")
|
|
392
|
+
exit()
|
|
393
|
+
|
|
394
|
+
if not self.solvate and self.cubic:
|
|
395
|
+
logger.warning("WARNING:--cubic only available with --solvate. Turned off!")
|
|
396
|
+
self.cubic = False
|
|
397
|
+
|
|
398
|
+
if self.solvate:
|
|
399
|
+
self.preoriented = True
|
|
400
|
+
z_cen = True
|
|
401
|
+
pdb_prefix = "solvated"
|
|
402
|
+
self.dist_wat = self.dist
|
|
403
|
+
else:
|
|
404
|
+
pdb_prefix = "bilayer"
|
|
405
|
+
z_cen = False
|
|
406
|
+
|
|
407
|
+
leaflet_z = self.leaflet # leaflet thickness
|
|
408
|
+
lip_offset = self.lip_offset
|
|
409
|
+
bound_tail = self.tailplane # plane that defines boundary to last carbon in aliphatic chain
|
|
410
|
+
bound_head = self.headplane # plane that defines boundary to polar head
|
|
411
|
+
lipids = self.lipids # lipids to be used
|
|
412
|
+
|
|
413
|
+
self.onlymembrane = False
|
|
414
|
+
if self.pdb is None or self.pdb.count("None") == len(self.pdb):
|
|
415
|
+
self.onlymembrane = True
|
|
416
|
+
else:
|
|
417
|
+
for pdb in self.pdb:
|
|
418
|
+
if not (pdb.endswith(".pdb") or pdb.endswith(".pqr")) and pdb != "None":
|
|
419
|
+
logger.critical("CRITICAL:\n The input file can only be in PDB or PQR formats.")
|
|
420
|
+
exit()
|
|
421
|
+
base_dir = os.path.dirname(script_path.rstrip(os.path.sep))
|
|
422
|
+
def resolve_input_path(path, label):
|
|
423
|
+
if path == "None":
|
|
424
|
+
return path
|
|
425
|
+
if os.path.exists(path):
|
|
426
|
+
return os.path.abspath(path)
|
|
427
|
+
if not os.path.isabs(path):
|
|
428
|
+
candidate = os.path.abspath(os.path.join(base_dir, path))
|
|
429
|
+
if os.path.exists(candidate):
|
|
430
|
+
logger.debug("Resolved %s path %s -> %s", label, path, candidate)
|
|
431
|
+
return candidate
|
|
432
|
+
logger.critical("CRITICAL:\n %s file not found: %s (cwd=%s)", label, path, os.getcwd())
|
|
433
|
+
exit()
|
|
434
|
+
self.pdb = [resolve_input_path(pdb, "PDB") for pdb in self.pdb]
|
|
435
|
+
if self.double_span and self.pdb is None:
|
|
436
|
+
logger.critical("CRITICAL:\n --double_span requires a PDB file as input")
|
|
437
|
+
exit()
|
|
438
|
+
elif self.double_span and len(self.pdb) == 1:
|
|
439
|
+
self.pdb.append("None")
|
|
440
|
+
|
|
441
|
+
self.outfile = self.output
|
|
442
|
+
if self.outfile is not None:
|
|
443
|
+
if not self.outfile.endswith(".pdb"):
|
|
444
|
+
self.outfile = self.outfile + ".pdb"
|
|
445
|
+
if not os.path.isabs(self.outfile):
|
|
446
|
+
self.outfile = self._out_path(self.outfile)
|
|
447
|
+
if not self.output and self.pdb is None:
|
|
448
|
+
self.outfile = self._out_path(pdb_prefix + "_only.pdb")
|
|
449
|
+
elif not self.output:
|
|
450
|
+
self.outfile = self._out_path(
|
|
451
|
+
pdb_prefix + "_" + "".join([os.path.basename(pdb)[:-4] for pdb in self.pdb]) + ".pdb"
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
if getattr(self, "packlog", None) and not os.path.isabs(self.packlog):
|
|
455
|
+
self.packlog = self._out_path(self.packlog)
|
|
456
|
+
|
|
457
|
+
if lipids is not None:
|
|
458
|
+
if self.martini:
|
|
459
|
+
insane_lipids = self._load_insane_lipids()
|
|
460
|
+
if insane_lipids:
|
|
461
|
+
missing = []
|
|
462
|
+
for lipid in lipids:
|
|
463
|
+
for leaf in lipid.split("//"):
|
|
464
|
+
for entry in leaf.split(":"):
|
|
465
|
+
if entry not in insane_lipids:
|
|
466
|
+
missing.append(entry)
|
|
467
|
+
if missing:
|
|
468
|
+
logger.critical(
|
|
469
|
+
"CRITICAL:\n Martini lipid(s) not found in Insane4MemPrO lipidsa: %s",
|
|
470
|
+
", ".join(sorted(set(missing))),
|
|
471
|
+
)
|
|
472
|
+
exit()
|
|
473
|
+
else:
|
|
474
|
+
logger.warning(
|
|
475
|
+
"WARNING:\n Insane4MemPrO lipids list not available; skipping --martini lipid validation."
|
|
476
|
+
)
|
|
477
|
+
if any(
|
|
478
|
+
[
|
|
479
|
+
self.parameters[l]["ext"]
|
|
480
|
+
for lipid in lipids
|
|
481
|
+
for leaf in lipid.split("//")
|
|
482
|
+
for l in leaf.split(":")
|
|
483
|
+
if l in self.parameters
|
|
484
|
+
]
|
|
485
|
+
):
|
|
486
|
+
logger.info("The Lipid Force Field extension lipid_ext is required for this system.")
|
|
487
|
+
saltcon = self.saltcon # salt concentration in M
|
|
488
|
+
if not self.salt:
|
|
489
|
+
if "--saltcon" in sys.argv and (self.distxy_fix is not None or self.pdb is not None):
|
|
490
|
+
logger.error("ERROR:\n You specified a salt concentration, but not the salt flag. Only neutralizing ions will be added")
|
|
491
|
+
saltcon = 0
|
|
492
|
+
if self.charge_imbalance != 0:
|
|
493
|
+
logger.error("ERROR:\n You specified a charge imbalance, but not the salt flag. No charge imbalance will be applied")
|
|
494
|
+
override_salt = self.salt_override
|
|
495
|
+
#Check for cation
|
|
496
|
+
if self.sirah:
|
|
497
|
+
self.ion_dict = {"K+":("siKW",1,"KW"),"Na+":("siNaW",1,"NaW"),"Ca2+":("siCaX",2,"CaX"),"Cl-":("siClW",-1,"ClW")}
|
|
498
|
+
else:
|
|
499
|
+
self.ion_dict = amber_ion_dict
|
|
500
|
+
if self.martini:
|
|
501
|
+
insane_solvents = {s.upper() for s in self._load_insane_solvents()}
|
|
502
|
+
self.ion_dict = {"NA": ("NA", 1, "NA"), "CL": ("CL", -1, "CL"), "CA": ("CA", 2, "CA")}
|
|
503
|
+
if insane_solvents:
|
|
504
|
+
def _ion_token(name):
|
|
505
|
+
return re.sub(r"[^A-Za-z]", "", name).upper()
|
|
506
|
+
for ion_name in (self.salt_c, self.salt_a):
|
|
507
|
+
token = _ion_token(ion_name)
|
|
508
|
+
if token and token not in insane_solvents:
|
|
509
|
+
logger.critical(
|
|
510
|
+
"CRITICAL:\n Martini ion %s not found in Insane4MemPrO solvents list.",
|
|
511
|
+
ion_name,
|
|
512
|
+
)
|
|
513
|
+
exit()
|
|
514
|
+
else:
|
|
515
|
+
logger.warning(
|
|
516
|
+
"WARNING:\n Insane4MemPrO solvents list not available; skipping Martini ion validation."
|
|
517
|
+
)
|
|
518
|
+
if self.salt_c not in self.ion_dict:
|
|
519
|
+
logger.error("ERROR:\n The specified cation option is no available at the moment")
|
|
520
|
+
exit()
|
|
521
|
+
else:
|
|
522
|
+
if self.ion_dict[self.salt_c][1] < 0:
|
|
523
|
+
logger.error("ERROR:\n The specified cation is really an anion")
|
|
524
|
+
exit()
|
|
525
|
+
cation = self.ion_dict[self.salt_c][0]
|
|
526
|
+
if self.salt_a not in self.ion_dict:
|
|
527
|
+
logger.error("ERROR:\n The specified cation option is no available at the moment")
|
|
528
|
+
exit()
|
|
529
|
+
else:
|
|
530
|
+
if self.ion_dict[self.salt_a][1] > 0:
|
|
531
|
+
logger.error("ERROR:\n The specified anion is really a cation")
|
|
532
|
+
exit()
|
|
533
|
+
anion = self.ion_dict[self.salt_a][0] #Maybe more alternatives at some point? ILs?
|
|
534
|
+
distance = self.dist # distance from the protein to the box boundaries to XY
|
|
535
|
+
distance_wat = self.dist_wat # minimum distance from the surface of the membrane to the box boundary on Z
|
|
536
|
+
|
|
537
|
+
if (self.distxy_fix is not None and self.dims != [0,0,0]):
|
|
538
|
+
logger.error("ERROR:\n --distxy_fix and --dims should not be used together! Use -h for help.")
|
|
539
|
+
exit()
|
|
540
|
+
|
|
541
|
+
asym = False
|
|
542
|
+
if self.distxy_fix is not None and self.dims == [0,0,0]:
|
|
543
|
+
self.dims = [self.distxy_fix,self.distxy_fix,0]
|
|
544
|
+
elif self.dims == [0,0,0]:
|
|
545
|
+
self.dims = None
|
|
546
|
+
else:
|
|
547
|
+
asym = True
|
|
548
|
+
|
|
549
|
+
if (self.distxy_fix is None and self.dims is None) and self.onlymembrane:
|
|
550
|
+
logger.error("ERROR:\n No PDB file given or fixed XY dimensions specified. Check --distxy_fix and --dims for help (-h/--help).")
|
|
551
|
+
exit()
|
|
552
|
+
if self.writeout is None:
|
|
553
|
+
if int(self.nloop) < 10 or int(self.nloop_all) < 10:
|
|
554
|
+
logger.error("ERROR:\n nloop and nloop_all have to be bigger than the writeout frequency (every 10 loops by default). You can modify this with --writeout")
|
|
555
|
+
exit()
|
|
556
|
+
else:
|
|
557
|
+
if int(self.nloop) < int(self.writeout) or int(self.nloop_all) < int(self.writeout):
|
|
558
|
+
logger.error("ERROR:\n nloop and nloop_all have to be bigger than the writeout frequency! Modify the used values.")
|
|
559
|
+
exit()
|
|
560
|
+
protonate = self.notprotonate
|
|
561
|
+
grid_calc = self.notgridvol
|
|
562
|
+
self.delete = not self.keep
|
|
563
|
+
|
|
564
|
+
if self.martini and protonate:
|
|
565
|
+
logger.info("Martini mode enabled; skipping protonation.")
|
|
566
|
+
protonate = False
|
|
567
|
+
|
|
568
|
+
# JSwails suggestion//Check if in a tty. Turn off progress bar if that's the case.
|
|
569
|
+
if not os.isatty(sys.stdin.fileno()):
|
|
570
|
+
self.noprogress = True
|
|
571
|
+
|
|
572
|
+
# Make a list with created files for later deletion
|
|
573
|
+
self.created = []
|
|
574
|
+
self.created_notrun = []
|
|
575
|
+
self.created_mempro = []
|
|
576
|
+
|
|
577
|
+
###############################################
|
|
578
|
+
###############################################
|
|
579
|
+
###############################################
|
|
580
|
+
|
|
581
|
+
############ SEARCH LIBS, PDB, MEMPRO ###################
|
|
582
|
+
|
|
583
|
+
if protonate and not self.pdb2pqr and pdb2pqr:
|
|
584
|
+
self.pdb2pqr = True
|
|
585
|
+
logger.debug("Defaulting to pdb2pqr for protonation.")
|
|
586
|
+
if protonate and not self.pdb2pqr:
|
|
587
|
+
logger.error("ERROR:\n Protonation requested, but pdb2pqr is not available.")
|
|
588
|
+
exit()
|
|
589
|
+
|
|
590
|
+
if not os.path.exists(os.path.join(script_path, rep)):
|
|
591
|
+
logger.critical("CRITICAL:\n The data folder for using the script is missing. Check the path for the script!")
|
|
592
|
+
exit()
|
|
593
|
+
|
|
594
|
+
if self.pdb is not None:
|
|
595
|
+
if self.pdb[0] == "None" and not self.onlymembrane:
|
|
596
|
+
logger.error("ERROR:\n Please specify first the protein PDB file!")
|
|
597
|
+
exit()
|
|
598
|
+
for pdb in self.pdb:
|
|
599
|
+
if not os.path.exists(pdb) and pdb != "None":
|
|
600
|
+
logger.error("ERROR:\n Either the options were wrongly used or the file "+pdb+" doesn't exist!")
|
|
601
|
+
exit()
|
|
602
|
+
|
|
603
|
+
mempro_cmd = self.mempro
|
|
604
|
+
if mempro_cmd is None:
|
|
605
|
+
mempro_cmd = (
|
|
606
|
+
shutil.which("mempro")
|
|
607
|
+
or shutil.which("MemPrO")
|
|
608
|
+
or shutil.which("MemPro")
|
|
609
|
+
)
|
|
610
|
+
if mempro_cmd:
|
|
611
|
+
self.mempro = mempro_cmd
|
|
612
|
+
else:
|
|
613
|
+
self.mempro = ""
|
|
614
|
+
|
|
615
|
+
if not self.mempro:
|
|
616
|
+
miss = (
|
|
617
|
+
"MemPrO not found. Protein orientation will not be available unless --preoriented is set.\n"
|
|
618
|
+
"Install it with: pip install packmol-memgen-minimal[mempro]"
|
|
619
|
+
)
|
|
620
|
+
logger.warning("\n"+len(miss)*"#"+"\n"+miss+"\n"+len(miss)*"#"+"\n")
|
|
621
|
+
self.mempro = False
|
|
622
|
+
|
|
623
|
+
# logger.info(self.mempro if self.mempro else "MemPrO not used")
|
|
624
|
+
|
|
625
|
+
if not os.path.exists(os.path.join(script_path, lib, "pdbremix")):
|
|
626
|
+
logger.warning("WARNING:PDBREMIX lib not available. Volume estimation will be done based on estimated density")
|
|
627
|
+
else:
|
|
628
|
+
grid_avail = True
|
|
629
|
+
|
|
630
|
+
if self.solvents is None:
|
|
631
|
+
self.solvents = "WAT"
|
|
632
|
+
if len(self.solvents.split(":")) == 1:
|
|
633
|
+
self.solvent_ratio = "1"
|
|
634
|
+
elif len(self.solvents.split(":")) > 1 and self.solvent_ratio is None:
|
|
635
|
+
logger.error("ERROR:\n If using solvent mixtures, a solvent ratio has to be specified")
|
|
636
|
+
exit()
|
|
637
|
+
if len(self.solvents.split(":")) != len(self.solvent_ratio.split(":")):
|
|
638
|
+
logger.error("ERROR:\n Amount of solvent types and solvent ratios doesn't fit! Check your input")
|
|
639
|
+
exit()
|
|
640
|
+
|
|
641
|
+
if self.sirah:
|
|
642
|
+
logger.info("Using parameters to pack a SIRAH system")
|
|
643
|
+
self.solvents = "siWT4"
|
|
644
|
+
self.solvent_ratio = "1"
|
|
645
|
+
if "--prot_rad" not in sys.argv:
|
|
646
|
+
self.prot_rad = 2
|
|
647
|
+
if "--tolerance" not in sys.argv:
|
|
648
|
+
self.tolerance = 3
|
|
649
|
+
|
|
650
|
+
martini_skip_solvent_params = False
|
|
651
|
+
for solvent in self.solvents.split(":"):
|
|
652
|
+
if solvent not in self.sparameters:
|
|
653
|
+
if solvent and self.martini:
|
|
654
|
+
martini_skip_solvent_params = True
|
|
655
|
+
insane_solvents = {s.upper() for s in self._load_insane_solvents()}
|
|
656
|
+
if insane_solvents:
|
|
657
|
+
if solvent not in insane_solvents:
|
|
658
|
+
logger.critical(
|
|
659
|
+
"CRITICAL:\n Martini solvent %s not found in Insane4MemPrO solvents list.",
|
|
660
|
+
solvent,
|
|
661
|
+
)
|
|
662
|
+
exit()
|
|
663
|
+
else:
|
|
664
|
+
logger.warning(
|
|
665
|
+
"WARNING:\n Insane4MemPrO solvents list not available; skipping Martini solvent validation."
|
|
666
|
+
)
|
|
667
|
+
else:
|
|
668
|
+
logger.error("ERROR:\n Selected solvent %s parameters not available. Check --available_solvents" % (solvent))
|
|
669
|
+
exit()
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
solvent_ratios = [float(ratio) for ratio in self.solvent_ratio.split(":")]
|
|
673
|
+
if self.martini and martini_skip_solvent_params:
|
|
674
|
+
solvent_density = None
|
|
675
|
+
solvent_con = None
|
|
676
|
+
else:
|
|
677
|
+
solvent_density = sum(
|
|
678
|
+
[
|
|
679
|
+
float(self.sparameters[solvent]["density"]) * solvent_ratios[i]
|
|
680
|
+
for i, solvent in enumerate(self.solvents.split(":"))
|
|
681
|
+
]
|
|
682
|
+
) / sum(solvent_ratios)
|
|
683
|
+
solvent_con = sum(
|
|
684
|
+
[
|
|
685
|
+
(solvent_ratios[i] * float(self.sparameters[solvent]["density"]) * avogadro)
|
|
686
|
+
/ (float(self.sparameters[solvent]["MW"]) * 10**24)
|
|
687
|
+
for i, solvent in enumerate(self.solvents.split(":"))
|
|
688
|
+
]
|
|
689
|
+
) / sum(solvent_ratios)
|
|
690
|
+
|
|
691
|
+
if lipids is None:
|
|
692
|
+
lipids = ["DOPC"]
|
|
693
|
+
if self.double and len(lipids) == 1:
|
|
694
|
+
lipids = lipids+["//".join(reversed(lipids[0].split("//")))]
|
|
695
|
+
elif self.double and len(lipids) != 1:
|
|
696
|
+
logger.error("ERROR:\n --double requires a single definition of lipid composition")
|
|
697
|
+
exit()
|
|
698
|
+
self.lipids = lipids # Keep the class list up to date
|
|
699
|
+
if self.double_span and len(lipids) != 2:
|
|
700
|
+
logger.error("ERROR:\n --double_span requires a definition of lipid composition per bilayer")
|
|
701
|
+
exit()
|
|
702
|
+
if self.ratio is None:
|
|
703
|
+
logger.info("No ratio provided (--ratio).")
|
|
704
|
+
self.ratio = ["1"]*len(lipids)
|
|
705
|
+
if self.double and len(lipids) == 2 and len(self.ratio) == 1:
|
|
706
|
+
self.ratio = self.ratio+["//".join(reversed(self.ratio[0].split("//")))]
|
|
707
|
+
|
|
708
|
+
if len(lipids) != len(self.ratio):
|
|
709
|
+
logger.error("ERROR:\n Number of defined bilayer lipids and ratios doesn't fit! Check your input")
|
|
710
|
+
exit()
|
|
711
|
+
if self.martini and self.build_system is not None and not (self.build_arguments and self.build_arguments.strip()):
|
|
712
|
+
auto_args = self._auto_build_insane_args()
|
|
713
|
+
if auto_args:
|
|
714
|
+
logger.info("Auto-constructed --insane_args: %s", auto_args)
|
|
715
|
+
self.build_arguments = auto_args
|
|
716
|
+
else:
|
|
717
|
+
logger.warning("WARNING:\n Could not auto-construct --insane_args; provide it explicitly.")
|
|
718
|
+
|
|
719
|
+
if self.n_ter is None and self.double:
|
|
720
|
+
self.n_ter = ["in","out"]
|
|
721
|
+
elif self.n_ter is None:
|
|
722
|
+
self.n_ter = ["in"]*len(lipids)
|
|
723
|
+
|
|
724
|
+
elif len(self.n_ter) != len(self.pdb):
|
|
725
|
+
logger.error("ERROR:\n Number of specified orientations and bilayers doesn't fit! Check your input")
|
|
726
|
+
exit()
|
|
727
|
+
else:
|
|
728
|
+
for n, pdb in enumerate(self.pdb):
|
|
729
|
+
if pdb != "None" and not (self.n_ter[n] == "in" or self.n_ter[n] == "out"):
|
|
730
|
+
logger.error("ERROR:\n The orientation has to be 'in' or 'out' unless pdb is 'None'")
|
|
731
|
+
exit()
|
|
732
|
+
|
|
733
|
+
if self.pdb is not None:
|
|
734
|
+
if self.charge_pdb_delta is None:
|
|
735
|
+
self.charge_pdb_delta = [0]*len(self.pdb)
|
|
736
|
+
elif len(self.charge_pdb_delta) != len(self.pdb):
|
|
737
|
+
logger.error("ERROR:\n Number of specified charge deltas and pdbs doesn't fit! Check your input")
|
|
738
|
+
exit()
|
|
739
|
+
else:
|
|
740
|
+
self.charge_pdb_delta = [int(i) for i in self.charge_pdb_delta]
|
|
741
|
+
|
|
742
|
+
if self.solute_prot_dist is not None and self.onlymembrane:
|
|
743
|
+
logger.error("ERROR:\n A solute distance can only be specified with respect to an included PDB!")
|
|
744
|
+
exit()
|
|
745
|
+
|
|
746
|
+
if self.channel_plug is not None and self.onlymembrane:
|
|
747
|
+
logger.error("ERROR:\n A channel plug can only be specified with respect to an included PDB!")
|
|
748
|
+
exit()
|
|
749
|
+
|
|
750
|
+
if self.solute_charge is None and self.solute is not None:
|
|
751
|
+
self.solute_charge = [0]*len(self.solute)
|
|
752
|
+
|
|
753
|
+
elif self.solute_charge is not None and self.solute is not None:
|
|
754
|
+
if len(self.solute_charge) != len(self.solute):
|
|
755
|
+
logger.error("ERROR:\n Number of specified solute charges and solutes doesn't fit! Check your input")
|
|
756
|
+
exit()
|
|
757
|
+
else:
|
|
758
|
+
for n, solute in enumerate(self.solute):
|
|
759
|
+
try:
|
|
760
|
+
self.solute_charge[n] = int(self.solute_charge[n])
|
|
761
|
+
except:
|
|
762
|
+
logger.error("ERROR:\n Solute charges have to be integers")
|
|
763
|
+
exit()
|
|
764
|
+
apl_offset = {}
|
|
765
|
+
composition = {}
|
|
766
|
+
self.sterols_PI_used = False
|
|
767
|
+
for bilayer in range(len(lipids)):
|
|
768
|
+
apl_offset[bilayer] = {}
|
|
769
|
+
composition[bilayer] = {}
|
|
770
|
+
if "//" in lipids[bilayer]:
|
|
771
|
+
if len(lipids[bilayer].split("//")) != 2 or len(self.ratio[bilayer].split("//")) != 2:
|
|
772
|
+
logger.error("ERROR:\n If different leaflet compositions used, ratios have to be specified explicitly, and both must be separated by one '//' only!")
|
|
773
|
+
exit()
|
|
774
|
+
for leaflet in range(2):
|
|
775
|
+
if len(lipids[bilayer].split("//")[leaflet].split(":")) != len(self.ratio[bilayer].split("//")[leaflet].split(":")):
|
|
776
|
+
logger.error("ERROR:\n Amount of lipid types and ratios doesn't fit! Check your input")
|
|
777
|
+
exit()
|
|
778
|
+
apl_offset[bilayer][leaflet] = {}
|
|
779
|
+
if self.apl_offset is None:
|
|
780
|
+
apl_offset[bilayer][leaflet] = 1.0
|
|
781
|
+
elif len(self.apl_offset) != len(lipids):
|
|
782
|
+
logger.error("ERROR:\n If apl_offset is specified, it has to be set once per bilayer")
|
|
783
|
+
elif "//" in self.apl_offset[bilayer]:
|
|
784
|
+
apl_offset[bilayer][leaflet] = float(self.apl_offset[bilayer].split("//")[leaflet])
|
|
785
|
+
else:
|
|
786
|
+
apl_offset[bilayer][leaflet] = float(self.apl_offset[bilayer])
|
|
787
|
+
composition[bilayer][leaflet]={}
|
|
788
|
+
ratio_total = sum([float(x) for x in self.ratio[bilayer].split("//")[leaflet].split(":")])
|
|
789
|
+
for n, lipid in enumerate(lipids[bilayer].split("//")[leaflet].split(":")):
|
|
790
|
+
if self.sirah:
|
|
791
|
+
composition[bilayer][leaflet]["si"+lipid] = float(self.ratio[bilayer].split("//")[leaflet].split(":")[n])/ratio_total
|
|
792
|
+
else:
|
|
793
|
+
composition[bilayer][leaflet][lipid] = float(self.ratio[bilayer].split("//")[leaflet].split(":")[n])/ratio_total
|
|
794
|
+
else:
|
|
795
|
+
if len(lipids[bilayer].split(":")) != len(self.ratio[bilayer].split(":")):
|
|
796
|
+
logger.error("ERROR:\n Amount of lipid types and ratios doesn't fit! Check your input")
|
|
797
|
+
exit()
|
|
798
|
+
if self.apl_offset is None:
|
|
799
|
+
apl_offset[bilayer][0] = apl_offset[bilayer][1] = 1.0
|
|
800
|
+
elif len(self.apl_offset) != len(lipids):
|
|
801
|
+
logger.error("ERROR:\n If apl_offset is specified, it has to be set once per bilayer")
|
|
802
|
+
elif "//" in self.apl_offset[bilayer]:
|
|
803
|
+
apl_offset[bilayer][0], apl_offset[bilayer][1] = map(float,self.apl_offset[bilayer].split("//"))
|
|
804
|
+
else:
|
|
805
|
+
apl_offset[bilayer][0] = apl_offset[bilayer][1] = float(self.apl_offset[bilayer])
|
|
806
|
+
composition[bilayer][0]={} ; composition[bilayer][1]={}
|
|
807
|
+
ratio_total = sum([float(x) for x in self.ratio[bilayer].split(":")])
|
|
808
|
+
for n, lipid in enumerate(lipids[bilayer].split(":")):
|
|
809
|
+
if self.sirah:
|
|
810
|
+
composition[bilayer][0]["si"+lipid] = composition[bilayer][1]["si"+lipid] = float(self.ratio[bilayer].split(":")[n])/ratio_total
|
|
811
|
+
else:
|
|
812
|
+
composition[bilayer][0][lipid] = composition[bilayer][1][lipid] = float(self.ratio[bilayer].split(":")[n])/ratio_total
|
|
813
|
+
|
|
814
|
+
for key in [x for leaflet in composition[bilayer] for bilayer in composition for x in composition[bilayer][leaflet]]:
|
|
815
|
+
if key in sterols_PI or "PI" in key:
|
|
816
|
+
self.sterols_PI_used = True
|
|
817
|
+
if key not in self.parameters:
|
|
818
|
+
logger.critical("CRITICAL:\n Parameters missing for "+key+". Please check the file memgen.parm")
|
|
819
|
+
exit()
|
|
820
|
+
if self.charmm and self.parameters[key]["charmm"] == "N":
|
|
821
|
+
logger.critical("CRITICAL:\n Lipid "+key+" not available in CHARMM format.")
|
|
822
|
+
exit()
|
|
823
|
+
if not os.path.exists(os.path.join(script_path, rep, "pdbs", key +".pdb")) and key+".pdb" not in pdbtar.getnames():
|
|
824
|
+
logger.warning("WARNING:\n PDB file for "+key+" not found in repo!")
|
|
825
|
+
if not os.path.exists(key+".pdb"):
|
|
826
|
+
logger.critical("CRITICAL:\n PDB file for "+key+" not found in local folder!")
|
|
827
|
+
exit()
|
|
828
|
+
|
|
829
|
+
if not self.onlymembrane:
|
|
830
|
+
if len(composition) != len(self.pdb):
|
|
831
|
+
if self.double and len(composition) == 2 and len(self.pdb) == 1:
|
|
832
|
+
logger.debug("Using the pdb file for both bilayers")
|
|
833
|
+
self.pdb = self.pdb*2
|
|
834
|
+
else:
|
|
835
|
+
logger.critical("CRITICAL:\n The number of provided PDB files doesnt fit with the number of bilayers. Pass --pdb \"None\" if you want an empty bilayer.")
|
|
836
|
+
exit()
|
|
837
|
+
|
|
838
|
+
if self.solute is not None:
|
|
839
|
+
if self.solute_con is not None:
|
|
840
|
+
if len(self.solute) != len(self.solute_con):
|
|
841
|
+
logger.critical("CRITICAL:\n Number of solutes and concentrations/number of molecules is not the same! Please provide a concentration for each solute in the respective order.")
|
|
842
|
+
exit()
|
|
843
|
+
else:
|
|
844
|
+
logger.error("ERROR:\n Concentrations/number of molecules have to be provided.")
|
|
845
|
+
exit()
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
if self.solute is not None:
|
|
849
|
+
for i, sol in enumerate(self.solute):
|
|
850
|
+
if not os.path.exists(self.solute[i]):
|
|
851
|
+
logger.error(self.solute[i]+" not found!")
|
|
852
|
+
exit()
|
|
853
|
+
logger.info("Extra solute PDB = %-9s" % (self.solute[i]))
|
|
854
|
+
logger.info("Solute to be added = %-9s" % (self.solute_con[i]))
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
############################### SCRIPT HEADER ################################
|
|
858
|
+
|
|
859
|
+
content_header = content_prot = content_lipid = content_solvent = content_ion = content_solute = ""
|
|
860
|
+
content_header += "tolerance "+str(self.tolerance)+"\n"
|
|
861
|
+
content_header += "filetype pdb\n"
|
|
862
|
+
content_header += "output " + self._packmol_path(self.outfile) + "\n\n"
|
|
863
|
+
if self.writeout is not None:
|
|
864
|
+
content_header += "writeout "+self.writeout+"\n"
|
|
865
|
+
if self.traj:
|
|
866
|
+
if self.writeout is None:
|
|
867
|
+
content_header += "writeout 1\n"
|
|
868
|
+
content_header += "writebad\n\n"
|
|
869
|
+
|
|
870
|
+
if self.random:
|
|
871
|
+
content_header += "seed -1\n"
|
|
872
|
+
if self.packall:
|
|
873
|
+
content_header += "packall\n"
|
|
874
|
+
if self.maxit != 20:
|
|
875
|
+
content_header += "maxit "+str(self.maxit)+"\n"
|
|
876
|
+
if self.movefrac != 0.05:
|
|
877
|
+
content_header += "movefrac "+str(self.movefrac)+"\n"
|
|
878
|
+
if self.short_penalty:
|
|
879
|
+
content_header += "use_short_tol\n"
|
|
880
|
+
content_header += "short_tol_dist "+str(self.tolerance/2)+"\n"
|
|
881
|
+
content_header += "short_tol_scale 5\n"
|
|
882
|
+
if not self.charmm:
|
|
883
|
+
content_header += "add_amber_ter\n"
|
|
884
|
+
if self.hexadecimal_indices:
|
|
885
|
+
content_header += "hexadecimal_indices\n"
|
|
886
|
+
content_header += "amber_ter_preserve\n"
|
|
887
|
+
if self.movebadrandom:
|
|
888
|
+
content_header += "movebadrandom\n"
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
content_header += "nloop "+str(self.nloop_all)+"\n\n"
|
|
892
|
+
|
|
893
|
+
pond_lip_vol_dict = {}
|
|
894
|
+
pond_lip_apl_dict = {}
|
|
895
|
+
lipnum_dict = {}
|
|
896
|
+
lipnum_area_dict = {}
|
|
897
|
+
|
|
898
|
+
X_min = X_max = X_len = Y_min = Y_max = Y_len = 0
|
|
899
|
+
Z_dim = []
|
|
900
|
+
memvol = []
|
|
901
|
+
solvol = []
|
|
902
|
+
charges = []
|
|
903
|
+
if not self.onlymembrane:
|
|
904
|
+
chain_nr = 0
|
|
905
|
+
chain_index = list(string.ascii_uppercase)+list(string.ascii_lowercase)
|
|
906
|
+
for n,pdb in enumerate(self.pdb):
|
|
907
|
+
if pdb != "None":
|
|
908
|
+
if not self.verbose:
|
|
909
|
+
logger.info("Preprocessing "+pdb+". This might take a minute.")
|
|
910
|
+
if self.martini and not self.martinized:
|
|
911
|
+
logger.debug("Running martinize2 for %s", pdb)
|
|
912
|
+
pdb = self._martinize2(pdb, overwrite=self.overwrite)
|
|
913
|
+
if not self.preoriented and self.mempro:
|
|
914
|
+
if self.double_span:
|
|
915
|
+
logger.debug("Attempting to orient double span protein using MemPrO...")
|
|
916
|
+
self._used_tools.add("mempro")
|
|
917
|
+
self._used_tools.add("martini")
|
|
918
|
+
pdb, z_offset_ds = self.mempro_align(pdb,keepligs=self.keepligs,verbose=self.verbose,overwrite=self.overwrite,n_ter=self.n_ter[n], double_span=True)
|
|
919
|
+
z_offset_ds = np.abs(z_offset_ds)
|
|
920
|
+
pdb_ds = pdb
|
|
921
|
+
else:
|
|
922
|
+
logger.debug("Orienting the protein using MemPrO...")
|
|
923
|
+
self._used_tools.add("mempro")
|
|
924
|
+
self._used_tools.add("martini")
|
|
925
|
+
pdb = self.mempro_align(pdb,keepligs=self.keepligs,verbose=self.verbose,overwrite=self.overwrite,n_ter=self.n_ter[n])
|
|
926
|
+
if self.martini and self.build_system is not None:
|
|
927
|
+
self._finalize_martini_build()
|
|
928
|
+
if self.keep_mempro:
|
|
929
|
+
self.created_mempro.append(pdb)
|
|
930
|
+
else:
|
|
931
|
+
self.created.append(pdb)
|
|
932
|
+
if protonate:
|
|
933
|
+
logger.debug("Adding protons using pdb2pqr at pH "+str(self.pdb2pqr_pH)+"...")
|
|
934
|
+
self._used_tools.add("pdb2pqr")
|
|
935
|
+
pdb = pdb2pqr_protonate(
|
|
936
|
+
pdb, overwrite=self.overwrite, pH=self.pdb2pqr_pH, output_dir=self.outdir
|
|
937
|
+
)
|
|
938
|
+
self.created.append(pdb)
|
|
939
|
+
elif self.preoriented and self.double_span:
|
|
940
|
+
ds_oriented = pdb_parse(pdb, onlybb=False)
|
|
941
|
+
try:
|
|
942
|
+
z_dist = ds_oriented[('MEM', 1, 'X')][('MEM', 1)][2]+ds_oriented[('MEM', 2, 'X')][('MEM', 2)][2]
|
|
943
|
+
except:
|
|
944
|
+
logger.critical("CRITICAL:\n If using a preoriented double spanning PDB, it has to include MEM atoms!")
|
|
945
|
+
exit()
|
|
946
|
+
if z_dist < 0:
|
|
947
|
+
ds_oriented = translate_pdb(ds_oriented,vec=[0,0,z_dist])
|
|
948
|
+
z_dist *= -1
|
|
949
|
+
del ds_oriented[('MEM', 1, 'X')]
|
|
950
|
+
del ds_oriented[('MEM', 2, 'X')]
|
|
951
|
+
|
|
952
|
+
ds_temp = self._out_path("ds_temp.pdb")
|
|
953
|
+
pdb_write(ds_oriented, outfile=ds_temp)
|
|
954
|
+
pdb = pdb_ds = ds_temp
|
|
955
|
+
|
|
956
|
+
z_offset_ds = z_dist
|
|
957
|
+
|
|
958
|
+
if grid_calc:
|
|
959
|
+
logger.debug("Estimating the volume by building a grid (PDBREMIX)...")
|
|
960
|
+
grid = self.pdbvol(pdb)
|
|
961
|
+
logger.debug("PDBREMIX grid done: grid_file=%s vol=%s", grid[0], grid[1])
|
|
962
|
+
self.created.append(grid[0])
|
|
963
|
+
else:
|
|
964
|
+
grid = (None,None)
|
|
965
|
+
|
|
966
|
+
############## FAST VALUE ESTIMATION FROM PDB #####################
|
|
967
|
+
mem_params = MembraneParams(
|
|
968
|
+
pdb,
|
|
969
|
+
leaflet_z,
|
|
970
|
+
grid[0],
|
|
971
|
+
move=True,
|
|
972
|
+
move_vec=self.translate,
|
|
973
|
+
xy_cen=self.noxy_cen,
|
|
974
|
+
z_cen=z_cen,
|
|
975
|
+
outpdb=self._out_path("PROT" + str(n) + ".pdb"),
|
|
976
|
+
chain=chain_index[chain_nr],
|
|
977
|
+
renumber=True,
|
|
978
|
+
)
|
|
979
|
+
minmax, max_rad, charge_prot, vol, memvol_up, memvol_down, solvol_up, solvol_down, density, mass, chains = mem_params.measure()
|
|
980
|
+
chain_nr += chains
|
|
981
|
+
charge_prot += self.charge_pdb_delta[n]
|
|
982
|
+
charges.append(charge_prot)
|
|
983
|
+
if self.double:
|
|
984
|
+
mem_params = MembraneParams(
|
|
985
|
+
pdb,
|
|
986
|
+
leaflet_z,
|
|
987
|
+
grid[0],
|
|
988
|
+
move=True,
|
|
989
|
+
move_vec=self.translate,
|
|
990
|
+
xy_cen=self.noxy_cen,
|
|
991
|
+
z_cen=z_cen,
|
|
992
|
+
outpdb=self._out_path("PROT" + str(n + 1) + ".pdb"),
|
|
993
|
+
chain=chain_index[chain_nr],
|
|
994
|
+
renumber=True,
|
|
995
|
+
)
|
|
996
|
+
minmax, max_rad, charge_prot, vol, memvol_up, memvol_down, solvol_up, solvol_down, density, mass, chains = mem_params.measure()
|
|
997
|
+
chain_nr += chains
|
|
998
|
+
charge_prot += self.charge_pdb_delta[n]
|
|
999
|
+
charges.append(charge_prot)
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
elif pdb == "None" and self.double_span:
|
|
1003
|
+
logger.debug("Estimating for second mem!")
|
|
1004
|
+
mem_params = MembraneParams(
|
|
1005
|
+
pdb_ds,
|
|
1006
|
+
leaflet_z,
|
|
1007
|
+
grid[0],
|
|
1008
|
+
move=True,
|
|
1009
|
+
move_vec=[0, 0, -z_offset_ds],
|
|
1010
|
+
xy_cen=self.noxy_cen,
|
|
1011
|
+
z_cen=z_cen,
|
|
1012
|
+
outpdb=self._out_path("PROT" + str(n) + ".pdb"),
|
|
1013
|
+
chain=chain_index[chain_nr],
|
|
1014
|
+
renumber=True,
|
|
1015
|
+
)
|
|
1016
|
+
minmax, max_rad, charge_prot, vol, memvol_up, memvol_down, solvol_up, solvol_down, density, mass, chains = mem_params.measure()
|
|
1017
|
+
else:
|
|
1018
|
+
if not self.verbose:
|
|
1019
|
+
logger.debug("Building membrane without protein!")
|
|
1020
|
+
if self.dims is not None:
|
|
1021
|
+
minmax = [-self.dims[0]/2,-self.dims[1]/2,-leaflet_z,self.dims[0]/2,self.dims[1]/2,leaflet_z]
|
|
1022
|
+
else:
|
|
1023
|
+
minmax = minmax[0:2]+[-leaflet_z]+minmax[3:5]+[leaflet_z]
|
|
1024
|
+
charge_prot = 0
|
|
1025
|
+
charges.append(charge_prot)
|
|
1026
|
+
vol = 0
|
|
1027
|
+
memvol_up = 0
|
|
1028
|
+
memvol_down = 0
|
|
1029
|
+
solvol_up = 0
|
|
1030
|
+
solvol_down = 0
|
|
1031
|
+
density = 0
|
|
1032
|
+
mass = 0
|
|
1033
|
+
|
|
1034
|
+
solvol.append((solvol_down,solvol_up))
|
|
1035
|
+
memvol.append((memvol_down,memvol_up))
|
|
1036
|
+
if self.double:
|
|
1037
|
+
memvol.append((memvol_down,memvol_up))
|
|
1038
|
+
# solvol.append((solvol_down,solvol_up))
|
|
1039
|
+
|
|
1040
|
+
if self.double_span and pdb == "None":
|
|
1041
|
+
charges.append(0)
|
|
1042
|
+
Z_dim.append((-z_offset_ds/2,z_max-z_offset_ds,z_max-z_offset_ds/2))
|
|
1043
|
+
continue
|
|
1044
|
+
|
|
1045
|
+
com = [(minmax[0]+minmax[3])/2,(minmax[1]+minmax[4])/2,(minmax[2]+minmax[5])/2]
|
|
1046
|
+
|
|
1047
|
+
if self.dims is not None:
|
|
1048
|
+
pdbx_min = com[0]-self.dims[0]/2
|
|
1049
|
+
pdbx_max = com[0]+self.dims[0]/2
|
|
1050
|
+
pdby_min = com[1]-self.dims[1]/2
|
|
1051
|
+
pdby_max = com[1]+self.dims[1]/2
|
|
1052
|
+
pdbz_min = com[2]-self.dims[2]/2
|
|
1053
|
+
pdbz_max = com[2]+self.dims[2]/2
|
|
1054
|
+
|
|
1055
|
+
else:
|
|
1056
|
+
pdbx_min = minmax[0]
|
|
1057
|
+
pdbx_max = minmax[3]
|
|
1058
|
+
pdby_min = minmax[1]
|
|
1059
|
+
pdby_max = minmax[4]
|
|
1060
|
+
pdbz_min = minmax[2]
|
|
1061
|
+
pdbz_max = minmax[5]
|
|
1062
|
+
|
|
1063
|
+
pdbx_len = pdbx_max-pdbx_min
|
|
1064
|
+
pdby_len = pdby_max-pdby_min
|
|
1065
|
+
pdbz_len = pdbz_max-pdbz_min
|
|
1066
|
+
|
|
1067
|
+
if not asym:
|
|
1068
|
+
if self.dims is None:
|
|
1069
|
+
# max_side_len = math.sqrt(pdbx_len**2+pdby_len**2)+2*distance
|
|
1070
|
+
max_side_len = 2*max_rad+2*distance
|
|
1071
|
+
else:
|
|
1072
|
+
max_side_len = max(self.dims[:2])
|
|
1073
|
+
if self.cubic:
|
|
1074
|
+
max_side_len = max(max_side_len,pdbz_len+2*distance)
|
|
1075
|
+
diff_x = max_side_len - pdbx_len
|
|
1076
|
+
diff_y = max_side_len - pdby_len
|
|
1077
|
+
diff_z = max_side_len - pdbz_len
|
|
1078
|
+
else:
|
|
1079
|
+
#if setting box dimensions by hand, check that box boundaries leave some space to add water
|
|
1080
|
+
if pdbz_min > -(leaflet_z+5):
|
|
1081
|
+
correct_z = pdbz_min+(leaflet_z+5)
|
|
1082
|
+
pdbz_min = -(leaflet_z+5)
|
|
1083
|
+
pdbz_max = pdbz_max-correct_z
|
|
1084
|
+
if pdbz_max < (leaflet_z+5):
|
|
1085
|
+
correct_z = pdbz_max-(leaflet_z+5)
|
|
1086
|
+
pdbz_max = (leaflet_z+5)
|
|
1087
|
+
pdbz_min = pdbz_min-correct_z
|
|
1088
|
+
if pdbz_max < minmax[5]+(self.dist_wat-5) or pdbz_min > minmax[2]-(self.dist_wat-5):
|
|
1089
|
+
logger.critical("CRITICAL: Set dimensions and protein location don't allow to have a proper water surface on the membrane. Increase the the used dimensions.")
|
|
1090
|
+
exit()
|
|
1091
|
+
diff_x = diff_y = diff_z = 0
|
|
1092
|
+
x_min = pdbx_min-diff_x/2
|
|
1093
|
+
x_max = pdbx_max+diff_x/2
|
|
1094
|
+
y_min = pdby_min-diff_y/2
|
|
1095
|
+
y_max = pdby_max+diff_y/2
|
|
1096
|
+
z_min = pdbz_min-diff_z/2
|
|
1097
|
+
z_max = pdbz_max+diff_z/2
|
|
1098
|
+
x_len = x_max-x_min
|
|
1099
|
+
y_len = y_max-y_min
|
|
1100
|
+
z_len = z_max-z_min
|
|
1101
|
+
|
|
1102
|
+
if not self.cubic and not asym:
|
|
1103
|
+
z_min = pdbz_min-distance_wat
|
|
1104
|
+
if z_min > -(leaflet_z+distance_wat):
|
|
1105
|
+
z_min = -(leaflet_z+distance_wat)
|
|
1106
|
+
z_max = pdbz_max+distance_wat
|
|
1107
|
+
if z_max < (leaflet_z+distance_wat):
|
|
1108
|
+
z_max = (leaflet_z+distance_wat)
|
|
1109
|
+
if self.xygauss:
|
|
1110
|
+
z_max += float(self.xygauss[2])
|
|
1111
|
+
z_len = z_max-z_min
|
|
1112
|
+
|
|
1113
|
+
prot_data2= """
|
|
1114
|
+
Estimated values for input protein:
|
|
1115
|
+
|
|
1116
|
+
Input PDB = %-9s
|
|
1117
|
+
Charge = %-9s
|
|
1118
|
+
Mass = %-9s Da
|
|
1119
|
+
Density = %-9s Da/A^3
|
|
1120
|
+
Estimated volume = %-9s A^3
|
|
1121
|
+
in upper leaflet = %-9s A^3
|
|
1122
|
+
in lower leaflet = %-9s A^3
|
|
1123
|
+
in upper water box = %-9s A^3
|
|
1124
|
+
in lower water box = %-9s A^3
|
|
1125
|
+
"""
|
|
1126
|
+
logger.debug(prot_data2 % ( pdb, charge_prot, mass, round(density,2), round(vol,2), round(memvol_up,2), round(memvol_down,2), round(solvol_up), round(solvol_down,2)))
|
|
1127
|
+
|
|
1128
|
+
|
|
1129
|
+
if x_len > X_len:
|
|
1130
|
+
X_min = x_min; X_max = x_max; X_len = x_len
|
|
1131
|
+
if y_len > Y_len:
|
|
1132
|
+
Y_min = y_min; Y_max = y_max; Y_len = y_len
|
|
1133
|
+
if self.double_span and pdb != "None":
|
|
1134
|
+
Z_dim.append((z_min,z_offset_ds/2,z_offset_ds/2-z_min))
|
|
1135
|
+
else:
|
|
1136
|
+
Z_dim.append((z_min,z_max,z_len))
|
|
1137
|
+
if self.double:
|
|
1138
|
+
Z_dim.append((z_min,z_max,z_len))
|
|
1139
|
+
break
|
|
1140
|
+
|
|
1141
|
+
else:
|
|
1142
|
+
if self.dims is not None:
|
|
1143
|
+
minmax = [-self.dims[0]/2,-self.dims[1]/2,-self.dims[2]/2,self.dims[0]/2,self.dims[1]/2,self.dims[2]/2]
|
|
1144
|
+
z_min = minmax[2]
|
|
1145
|
+
z_max = minmax[5]
|
|
1146
|
+
else:
|
|
1147
|
+
z_min = minmax[2]-distance_wat
|
|
1148
|
+
z_max = minmax[5]+distance_wat
|
|
1149
|
+
|
|
1150
|
+
charge_prot = 0
|
|
1151
|
+
charges = [charge_prot]*len(composition)
|
|
1152
|
+
vol = 0
|
|
1153
|
+
memvol_up = 0
|
|
1154
|
+
memvol_down = 0
|
|
1155
|
+
solvol_up = 0
|
|
1156
|
+
solvol_down = 0
|
|
1157
|
+
density = 0
|
|
1158
|
+
mass = 0
|
|
1159
|
+
|
|
1160
|
+
memvol = [(memvol_down,memvol_up)]*len(composition)
|
|
1161
|
+
solvol = [(solvol_down,solvol_up)]*len(composition)
|
|
1162
|
+
|
|
1163
|
+
pdbx_len = minmax[3]-minmax[0]
|
|
1164
|
+
pdby_len = minmax[4]-minmax[1]
|
|
1165
|
+
|
|
1166
|
+
if not asym:
|
|
1167
|
+
max_side_len = max(pdbx_len,pdby_len)
|
|
1168
|
+
diff_x = max_side_len - pdbx_len
|
|
1169
|
+
diff_y = max_side_len - pdby_len
|
|
1170
|
+
else:
|
|
1171
|
+
diff_x = diff_y = 0
|
|
1172
|
+
x_min = minmax[0]-diff_x/2
|
|
1173
|
+
x_max = minmax[3]+diff_x/2
|
|
1174
|
+
y_min = minmax[1]-diff_y/2
|
|
1175
|
+
y_max = minmax[4]+diff_y/2
|
|
1176
|
+
x_len = x_max-x_min
|
|
1177
|
+
y_len = y_max-y_min
|
|
1178
|
+
|
|
1179
|
+
|
|
1180
|
+
if not self.solvate:
|
|
1181
|
+
if z_min > -(leaflet_z+distance_wat):
|
|
1182
|
+
z_min = -(leaflet_z+distance_wat)
|
|
1183
|
+
if z_max < (leaflet_z+distance_wat):
|
|
1184
|
+
z_max = (leaflet_z+distance_wat)
|
|
1185
|
+
if self.xygauss:
|
|
1186
|
+
z_max += float(self.xygauss[2])
|
|
1187
|
+
z_len = z_max-z_min
|
|
1188
|
+
|
|
1189
|
+
if x_len > X_len:
|
|
1190
|
+
X_min = x_min; X_max = x_max; X_len = x_len
|
|
1191
|
+
if y_len > Y_len:
|
|
1192
|
+
Y_min = y_min; Y_max = y_max; Y_len = y_len
|
|
1193
|
+
Z_dim = [(z_min,z_max,z_len)]*len(composition)
|
|
1194
|
+
|
|
1195
|
+
if X_min < -1000 or X_max > 1000 or Y_min < -1000 or Y_max > 1000 or Z_dim[0][0] < -1000 or Z_dim[0][0]+sum([zdim[2] for zdim in Z_dim]) > 1000:
|
|
1196
|
+
logger.warning("WARNING:The size of the system is bigger than the default accepted values for PACKMOL. The flag sidemax will be added.")
|
|
1197
|
+
content_header += "sidemax "+str(max(abs(max(X_max,Y_max,Z_dim[0][0]+sum([zdim[2] for zdim in Z_dim]))),abs(min(X_min,Y_min,Z_dim[0][0]))))+"\n"
|
|
1198
|
+
|
|
1199
|
+
if self.pbc:
|
|
1200
|
+
content_header += f"pbc {X_min:.2f} {Y_min:.2f} {Z_dim[0][0]:.2f} {X_max:.2f} {Y_max:.2f} {Z_dim[0][0]+sum([zdim[2] for zdim in Z_dim]):.2f} \n\n"
|
|
1201
|
+
|
|
1202
|
+
prot_data1 = """
|
|
1203
|
+
Information for packing:
|
|
1204
|
+
|
|
1205
|
+
Input PDB(s) = %-9s
|
|
1206
|
+
Output PDB = %-9s
|
|
1207
|
+
Packmol output and log prefix = %-9s
|
|
1208
|
+
Lipids = %-9s
|
|
1209
|
+
Lipid ratio = %-9s
|
|
1210
|
+
Solvents = %-9s
|
|
1211
|
+
Solvent ratio = %-9s
|
|
1212
|
+
Salt concentration (M)= %-9s
|
|
1213
|
+
Distance to boundaries (A)= %-9s
|
|
1214
|
+
Minimum water distance (A)= %-9s
|
|
1215
|
+
Packmol loops = %-9s
|
|
1216
|
+
Packmol loops for All-together = %-9s"""
|
|
1217
|
+
|
|
1218
|
+
if not self.solvate:
|
|
1219
|
+
logger.info(prot_data1 % ( self.pdb, self.outfile, self.packlog, lipids, self.ratio, self.solvents, self.solvent_ratio, saltcon, distance, distance_wat, self.nloop, self.nloop_all))
|
|
1220
|
+
else:
|
|
1221
|
+
logger.info(prot_data1 % ( self.pdb, self.outfile, self.packlog, "-", "-", self.solvents, self.solvent_ratio, saltcon, distance, distance_wat, self.nloop, self.nloop_all))
|
|
1222
|
+
if self.curvature is not None:
|
|
1223
|
+
logger.info(" Membrane curvature (1/A)= %-9s " % (self.curvature))
|
|
1224
|
+
elif self.xygauss:
|
|
1225
|
+
logger.info(" XY Gauss curvature (s_x,s_y,h) = %s %s %s " % tuple(self.xygauss))
|
|
1226
|
+
|
|
1227
|
+
box_info = """
|
|
1228
|
+
Box information:
|
|
1229
|
+
x_min = %-9s
|
|
1230
|
+
x_max = %-9s
|
|
1231
|
+
x_len = %-9s
|
|
1232
|
+
|
|
1233
|
+
y_min = %-9s
|
|
1234
|
+
y_max = %-9s
|
|
1235
|
+
y_len = %-9s
|
|
1236
|
+
|
|
1237
|
+
z_min = %-9s
|
|
1238
|
+
z_max = %-9s
|
|
1239
|
+
z_len = %-9s
|
|
1240
|
+
"""
|
|
1241
|
+
logger.debug(box_info % ( X_min, X_max, X_len, Y_min, Y_max, Y_len, Z_dim[0][0], Z_dim[0][0]+sum([zdim[2] for zdim in Z_dim]), sum([zdim[2] for zdim in Z_dim]) ))
|
|
1242
|
+
self.X_len = X_len
|
|
1243
|
+
self.Y_len = Y_len
|
|
1244
|
+
self.Z_dim = Z_dim
|
|
1245
|
+
|
|
1246
|
+
if os.path.exists(self.outfile) and not self.overwrite:
|
|
1247
|
+
logger.info("Packed PDB "+self.outfile+" found. Skipping PACKMOL")
|
|
1248
|
+
return False
|
|
1249
|
+
else:
|
|
1250
|
+
z_offset = 0
|
|
1251
|
+
|
|
1252
|
+
for n, bilayer in enumerate(composition):
|
|
1253
|
+
if bilayer > 0:
|
|
1254
|
+
if self.double_span:
|
|
1255
|
+
z_offset = z_offset_ds
|
|
1256
|
+
else:
|
|
1257
|
+
z_offset = z_offset+Z_dim[bilayer-1][1]-Z_dim[bilayer][0]
|
|
1258
|
+
|
|
1259
|
+
################################## PROTEIN ###################################
|
|
1260
|
+
|
|
1261
|
+
if not self.onlymembrane:
|
|
1262
|
+
if self.pdb[bilayer] != "None":
|
|
1263
|
+
prot_path = self._out_path("PROT" + str(bilayer) + ".pdb")
|
|
1264
|
+
self.created_notrun.append(prot_path)
|
|
1265
|
+
if self.sirah:
|
|
1266
|
+
prot_cg_path = self._out_path("PROT" + str(bilayer) + "_cg.pdb")
|
|
1267
|
+
self.created_notrun.append(prot_cg_path)
|
|
1268
|
+
content_prot += "structure " + self._packmol_path(prot_cg_path) + "\n"
|
|
1269
|
+
else:
|
|
1270
|
+
content_prot += "structure " + self._packmol_path(prot_path) + "\n"
|
|
1271
|
+
content_prot += " number 1\n"
|
|
1272
|
+
content_prot += " fixed 0. 0. " + str(z_offset) + " 0. 0. 0.\n"
|
|
1273
|
+
content_prot += " radius " + str(self.prot_rad) + "\n"
|
|
1274
|
+
content_prot += "end structure\n\n"
|
|
1275
|
+
|
|
1276
|
+
################################ LIPIDS ######################################
|
|
1277
|
+
|
|
1278
|
+
if self.curvature is not None:
|
|
1279
|
+
lipid_vol_up = sphere_integral_square(X_min,X_max,Y_min,Y_max,r1=sphere_radius+z_offset,r2=sphere_radius+z_offset+leaflet_z,c=-sphere_radius)-memvol[bilayer][1]
|
|
1280
|
+
lipid_vol_down = sphere_integral_square(X_min,X_max,Y_min,Y_max,r1=sphere_radius+z_offset-leaflet_z,r2=sphere_radius+z_offset,c=-sphere_radius)-memvol[bilayer][0]
|
|
1281
|
+
elif self.xygauss:
|
|
1282
|
+
lipid_vol_up = gauss_integral_square(X_min,X_max,Y_min,Y_max,*map(float,self.xygauss),g1=z_offset,g2=z_offset+leaflet_z)-memvol[bilayer][1]
|
|
1283
|
+
lipid_vol_down = gauss_integral_square(X_min,X_max,Y_min,Y_max,*map(float,self.xygauss),g1=z_offset-leaflet_z,g2=z_offset)-memvol[bilayer][0]
|
|
1284
|
+
else:
|
|
1285
|
+
lipid_vol_up = ((X_len+2*lip_offset)*(Y_len+2*lip_offset)*leaflet_z)-memvol[bilayer][1]
|
|
1286
|
+
lipid_vol_down = ((X_len+2*lip_offset)*(Y_len+2*lip_offset)*leaflet_z)-memvol[bilayer][0]
|
|
1287
|
+
lipid_vol = (lipid_vol_down,lipid_vol_up)
|
|
1288
|
+
lipid_area = X_len*Y_len # if curvature, will be defined in loop
|
|
1289
|
+
if self.xygauss:
|
|
1290
|
+
lipid_area = gauss_rectangle_area(X_min,X_max,Y_min,Y_max, *map(float,self.xygauss))
|
|
1291
|
+
pond_lip_vol_dict[bilayer] = {}
|
|
1292
|
+
pond_lip_apl_dict[bilayer] = {}
|
|
1293
|
+
lipnum_dict[bilayer] = {}
|
|
1294
|
+
lipnum_area_dict[bilayer] = {}
|
|
1295
|
+
for leaflet in composition[bilayer]:
|
|
1296
|
+
if self.curvature is not None:
|
|
1297
|
+
if leaflet < 1:
|
|
1298
|
+
#lipid_area = sphere_rectangle_area(sphere_radius+z_offset-leaflet_z,sphere_dist(sphere_radius,X_len),sphere_dist(sphere_radius,Y_len))
|
|
1299
|
+
lipid_area = sphere_rectangle_area(sphere_radius+z_offset-leaflet_z,X_len,Y_len)
|
|
1300
|
+
logger.debug("Curvature lower leaflet area:"+str(lipid_area))
|
|
1301
|
+
else:
|
|
1302
|
+
#lipid_area = sphere_rectangle_area(sphere_radius+z_offset+leaflet_z,sphere_dist(sphere_radius,X_len),sphere_dist(sphere_radius,Y_len))
|
|
1303
|
+
lipid_area = sphere_rectangle_area(sphere_radius+z_offset+leaflet_z,X_len,Y_len)
|
|
1304
|
+
logger.debug("Curvature upper leaflet area:"+str(lipid_area))
|
|
1305
|
+
pond_lip_vol = 0
|
|
1306
|
+
pond_lip_apl = 0
|
|
1307
|
+
for lipid in composition[bilayer][leaflet]:
|
|
1308
|
+
if self.parameters[lipid]["V"] == "XXXX":
|
|
1309
|
+
if self.parameters[lipid]["C"] == "X":
|
|
1310
|
+
logger.critical("CRITICAL:\n Volume not specified and can not be estimated! Check the parameter file")
|
|
1311
|
+
exit()
|
|
1312
|
+
nCH3,nCH2,nCH = list(map(int,self.parameters[lipid]["C"].split(":")))
|
|
1313
|
+
self.parameters[lipid]["V"] = str(int(round(float(self.parameters[lipid]["VH"])+nCH3*2*VCH2+nCH2*VCH2+nCH*VCH))) #doi:10.1016/j.bbamem.2005.07.006
|
|
1314
|
+
logger.debug("Experimental value for "+lipid+" volume not available in parm file. Using estimated "+self.parameters[lipid]["V"]+" A^3 instead...")
|
|
1315
|
+
if self.parameters[lipid][apl] == "XX":
|
|
1316
|
+
try:
|
|
1317
|
+
self.parameters[lipid][apl] = max([float(self.parameters[lip][apl]) for lip in self.parameters if (is_number(self.parameters[lip][apl]) and lip.endswith(lipid[-2:]))])
|
|
1318
|
+
logger.debug("Taking maximal APL of lipids with headgroup "+lipid[-2:])
|
|
1319
|
+
except:
|
|
1320
|
+
self.parameters[lipid][apl] = "75"
|
|
1321
|
+
logger.debug("No other lipid with same headgroup has APL. Setting APL of 75 to "+lipid[-2:])
|
|
1322
|
+
logger.debug("Value for "+lipid+" area per lipid not available in parm file. Using "+str(self.parameters[lipid][apl])+" A^2 instead...")
|
|
1323
|
+
pond_lip_vol += composition[bilayer][leaflet][lipid]*int(self.parameters[lipid]["V"])
|
|
1324
|
+
pond_lip_apl += composition[bilayer][leaflet][lipid]*int(self.parameters[lipid][apl])
|
|
1325
|
+
pond_lip_apl = pond_lip_apl*apl_offset[bilayer][leaflet]
|
|
1326
|
+
lipnum = lipid_vol[leaflet]/pond_lip_vol
|
|
1327
|
+
lipnum_area = lipid_area/pond_lip_apl-(memvol[bilayer][leaflet]/pond_lip_vol)
|
|
1328
|
+
pond_lip_vol_dict[bilayer][leaflet] = pond_lip_vol
|
|
1329
|
+
pond_lip_apl_dict[bilayer][leaflet] = pond_lip_apl
|
|
1330
|
+
lipnum_dict[bilayer][leaflet] = lipnum
|
|
1331
|
+
lipnum_area_dict[bilayer][leaflet] = lipnum_area
|
|
1332
|
+
|
|
1333
|
+
charge_lip = charge_solute = 0
|
|
1334
|
+
if not self.vol:
|
|
1335
|
+
lipnum_dict = lipnum_area_dict
|
|
1336
|
+
|
|
1337
|
+
################################ SOLUTE ######################################
|
|
1338
|
+
|
|
1339
|
+
if not self.solvate:
|
|
1340
|
+
if self.solute is not None and self.solute_inmem:
|
|
1341
|
+
for i,sol in enumerate(self.solute):
|
|
1342
|
+
logger.info("Adding "+self.solute_con[i]+" "+self.solute[i]+" to the lipid volume")
|
|
1343
|
+
grid_file, solute_vol = self.pdbvol(self.solute[i])
|
|
1344
|
+
self.created.append(grid_file)
|
|
1345
|
+
if self.solute_con[i].endswith("M") and is_number(self.solute_con[i][:-1]):
|
|
1346
|
+
solute_num = int(float(self.solute_con[i][:-1])*((sum(lipid_vol)*avogadro/(1*10**27))))
|
|
1347
|
+
elif self.solute_con[i].endswith("%") and is_number(self.solute_con[i][:-1]):
|
|
1348
|
+
solute_num = int(float(self.solute_con[i][:-1])/100*((sum(lipid_vol)/solute_vol)))
|
|
1349
|
+
solute_vol_tot = int(float(self.solute_con[i][:-1])/100*(sum(lipid_vol)))
|
|
1350
|
+
elif is_number(self.solute_con[i]):
|
|
1351
|
+
try:
|
|
1352
|
+
int(self.solute_con[i])
|
|
1353
|
+
except:
|
|
1354
|
+
logger.error("ERROR:\n A number less than 1 is specified. If a concentration was intended, add M/% as a suffix!")
|
|
1355
|
+
exit()
|
|
1356
|
+
solute_num = int(self.solute_con[i])
|
|
1357
|
+
else:
|
|
1358
|
+
logger.error("ERROR:\n The format used to specify the number of molecules is not correct! It has to be a number or a number with an M/% suffix.")
|
|
1359
|
+
exit()
|
|
1360
|
+
if solute_num < 1:
|
|
1361
|
+
logger.error("ERROR:\n The solute concentration is too low for the calculated box size. Try increasing the concentration.")
|
|
1362
|
+
exit()
|
|
1363
|
+
if not self.solute_con[i].endswith("%"):
|
|
1364
|
+
solute_vol_tot = solute_vol*solute_num
|
|
1365
|
+
charge_solute += self.solute_charge[i]*solute_num
|
|
1366
|
+
for leaflet in lipnum_dict[bilayer]:
|
|
1367
|
+
lipnum_dict[bilayer][leaflet] -= int((solute_vol_tot)/(2*pond_lip_vol_dict[bilayer][leaflet]))
|
|
1368
|
+
|
|
1369
|
+
content_solute += "structure "+self.solute[i]+"\n"
|
|
1370
|
+
content_solute += " number "+str(int(solute_num))+"\n"
|
|
1371
|
+
if self.solute_prot_dist is not None and self.pdb[bilayer] != "None":
|
|
1372
|
+
content_solute += " outside cylinder 0. 0. "+str(minmax[2]-self.solute_prot_dist)+" 0. 0. 1. "+str(max_rad+self.solute_prot_dist)+" "+str(minmax[5]-minmax[2]+self.solute_prot_dist)+" \n"
|
|
1373
|
+
if self.curvature is not None:
|
|
1374
|
+
if not self.pbc:
|
|
1375
|
+
content_solute += " inside box "+str(round(X_min,2))+" "+str(round(Y_min,2))+" "+str(round(Z_dim[bilayer][0]+z_offset,2))+" "+str(round(X_max,2))+" "+str(round(Y_max,2))+" "+str(Z_dim[bilayer][1])+"\n"
|
|
1376
|
+
content_solute += " inside sphere 0. 0. "+str(-sphere_radius)+" "+str(z_offset+sphere_radius+leaflet_z)+"\n"
|
|
1377
|
+
content_solute += " outside sphere 0. 0. "+str(-sphere_radius)+" "+str(z_offset+sphere_radius-leaflet_z)+"\n"
|
|
1378
|
+
elif self.xygauss:
|
|
1379
|
+
if not self.pbc:
|
|
1380
|
+
content_solute += " inside box "+str(round(X_min,2))+" "+str(round(Y_min,2))+" "+str(round(Z_dim[bilayer][0]+z_offset,2))+" "+str(round(X_max,2))+" "+str(round(Y_max,2))+" "+str(Z_dim[bilayer][1])+"\n"
|
|
1381
|
+
content_solute += " below xygauss 0. 0. "+self.xygauss[0]+" "+self.xygauss[1]+" "+str(z_offset+leaflet_z)+" "+self.xygauss[2]+"\n"
|
|
1382
|
+
content_solute += " over xygauss 0. 0. "+self.xygauss[0]+" "+self.xygauss[1]+" "+str(z_offset-leaflet_z)+" "+self.xygauss[2]+"\n"
|
|
1383
|
+
elif not self.pbc:
|
|
1384
|
+
content_solute += " inside box "+str(round(X_min*lip_offset,2))+" "+str(round(Y_min*lip_offset,2))+" "+str(z_offset-leaflet_z)+" "+str(round(X_max*lip_offset,2))+" "+str(round(Y_max*lip_offset,2))+" "+str(z_offset+leaflet_z)+"\n"
|
|
1385
|
+
content_solute += "end structure\n\n"
|
|
1386
|
+
elif (self.solute is not None or self.solute_con is not None) and self.solute_inmem:
|
|
1387
|
+
logger.error("ERROR:\n The solute parameters are incomplete. Please include both a solute pdb and a concentration.")
|
|
1388
|
+
exit()
|
|
1389
|
+
|
|
1390
|
+
|
|
1391
|
+
if not self.solvate:
|
|
1392
|
+
for leaflet in composition[bilayer]:
|
|
1393
|
+
for lipid in sorted(composition[bilayer][leaflet], reverse=True, key=lambda x: composition[bilayer][leaflet][x]):
|
|
1394
|
+
if self.headplane is None:
|
|
1395
|
+
bound_head=float(self.parameters[lipid]["h_bound"])
|
|
1396
|
+
if bound_head >= leaflet_z:
|
|
1397
|
+
logger.error("ERROR:\n The boundary for "+lipid+" head group is out of the space delimited for the membrane by the membrane width "+leaflet_z+". Please consider increasing the value!")
|
|
1398
|
+
exit()
|
|
1399
|
+
if self.tailplane is None:
|
|
1400
|
+
bound_tail=float(self.parameters[lipid]["t_bound"])
|
|
1401
|
+
if bound_tail <=0:
|
|
1402
|
+
logger.error("ERROR:\n The boundary for "+lipid+" tail is out of the space delimited for the membrane by the membrane center at the z axis origin (it must be a positive value greater than 0). Please consider increasing the value!")
|
|
1403
|
+
exit()
|
|
1404
|
+
lipid_pdb = self._out_path(lipid + ".pdb")
|
|
1405
|
+
if not os.path.isfile(lipid_pdb):
|
|
1406
|
+
try:
|
|
1407
|
+
shutil.copy(script_path + rep + "/pdbs/" + lipid + ".pdb", lipid_pdb)
|
|
1408
|
+
except:
|
|
1409
|
+
pdbtar.extract(lipid + ".pdb", path=self.outdir)
|
|
1410
|
+
self.created_notrun.append(lipid_pdb)
|
|
1411
|
+
|
|
1412
|
+
content_lipid += "structure " + self._packmol_path(lipid_pdb) + "\n"
|
|
1413
|
+
sp = 0
|
|
1414
|
+
if lipid in sterols_PI or "PI" in lipid: ## Avoid ring piercing by lipid tails, adding extra radius. Later remove those piercing despite this (find_piercing_lipids). Add inositol rings as well!
|
|
1415
|
+
if "PI" in lipid:
|
|
1416
|
+
_pdb = pdb_parse_TER(lipid_pdb, onlybb=False)
|
|
1417
|
+
_atom_string = " ".join([ str(_a[3]) for _a in _pdb[list(_pdb.keys())[0]].keys() if _a[0] == "PI" and _a[2] in PI_ring_probe])
|
|
1418
|
+
content_lipid += f" atoms {_atom_string}\n"
|
|
1419
|
+
content_lipid += " radius 1.5\n"
|
|
1420
|
+
content_lipid += " end atoms\n"
|
|
1421
|
+
else:
|
|
1422
|
+
content_lipid += " radius 1.5\n"
|
|
1423
|
+
if sum([composition[bilayer][leaflet][l] for l in composition[bilayer][leaflet] if l in sterols_PI or "PI" in l]) < 0.5 and (X_len > 50 and Y_len > 50): # only enable if less then half of the composition of the leaflet are sterols
|
|
1424
|
+
logger.info("Adding 5A padding to the sterol or PI %s components to avoid piercing in the periodic boundary" % (lipid))
|
|
1425
|
+
sp = 5 #Safety padding to avoid having sterols in the system rim and piercing from periodic image. Taking about "a lipid" width
|
|
1426
|
+
else:
|
|
1427
|
+
logger.warning("Sterols and/or PI are used, and the concentration is too high or the system too small to add proper side padding. Check that your system doesn't have pierced sterols or PI in the periodic boundary once the packing is finished")
|
|
1428
|
+
content_lipid += " nloop "+str(self.nloop)+"\n"
|
|
1429
|
+
if int(round(composition[bilayer][leaflet][lipid]*lipnum_dict[bilayer][leaflet])) < 1:
|
|
1430
|
+
logger.error("ERROR:\n The ratio for lipid "+lipid+" is too small for the given/estimated system size. Either increase the ratio or make the system bigger!")
|
|
1431
|
+
exit()
|
|
1432
|
+
content_lipid += " number "+str(int(round(composition[bilayer][leaflet][lipid]*lipnum_dict[bilayer][leaflet])))+"\n"
|
|
1433
|
+
|
|
1434
|
+
if not self.pbc:
|
|
1435
|
+
if self.curvature is not None or self.xygauss or self.self_assembly:
|
|
1436
|
+
content_lipid += " inside box "+str(round(X_min*lip_offset,2)+sp)+" "+str(round(Y_min*lip_offset,2)+sp)+" "+str(Z_dim[bilayer][0]+z_offset)+" "+str(round(X_max*lip_offset,2)-sp)+" "+str(round(Y_max*lip_offset,2)-sp)+" "+str(Z_dim[bilayer][1]+z_offset)+"\n"
|
|
1437
|
+
else:
|
|
1438
|
+
content_lipid += " inside box "+str(round(X_min*lip_offset,2)+sp)+" "+str(round(Y_min*lip_offset,2)+sp)+" "+str(z_offset-leaflet_z+leaflet_z*leaflet)+" "+str(round(X_max*lip_offset,2)-sp)+" "+str(round(Y_max*lip_offset,2)-sp)+" "+str(z_offset+leaflet_z*leaflet)+"\n"
|
|
1439
|
+
|
|
1440
|
+
if self.channel_plug is not None and self.pdb[bilayer] != "None":
|
|
1441
|
+
if self.channel_plug == 0:
|
|
1442
|
+
self.channel_plug = max_rad/2
|
|
1443
|
+
content_lipid += " outside cylinder 0. 0. "+str(minmax[2])+" 0. 0. 1. "+str(self.channel_plug)+" "+str(minmax[5]-minmax[2])+" \n"
|
|
1444
|
+
|
|
1445
|
+
if not self.self_assembly:
|
|
1446
|
+
content_lipid += " atoms "+" ".join([p_atm for p_atm in self.parameters[lipid]["p_atm"].split(",")])+"\n"
|
|
1447
|
+
if leaflet == 0:
|
|
1448
|
+
if self.curvature is not None:
|
|
1449
|
+
content_lipid += " inside sphere 0. 0. "+str(-sphere_radius)+" "+str(sphere_radius-bound_head+z_offset)+"\n"
|
|
1450
|
+
elif self.xygauss:
|
|
1451
|
+
content_lipid += " below xygauss 0. 0. "+self.xygauss[0]+" "+self.xygauss[1]+" "+str(-bound_head+z_offset)+" "+self.xygauss[2]+"\n"
|
|
1452
|
+
else:
|
|
1453
|
+
content_lipid += " below plane 0. 0. 1. "+str(-bound_head+z_offset)+"\n"
|
|
1454
|
+
else:
|
|
1455
|
+
if self.curvature is not None:
|
|
1456
|
+
content_lipid += " outside sphere 0. 0. "+str(-sphere_radius)+" "+str(sphere_radius+bound_head+z_offset)+"\n"
|
|
1457
|
+
elif self.xygauss:
|
|
1458
|
+
content_lipid += " over xygauss 0. 0. "+self.xygauss[0]+" "+self.xygauss[1]+" "+str(bound_head+z_offset)+" "+self.xygauss[2]+"\n"
|
|
1459
|
+
else:
|
|
1460
|
+
content_lipid += " over plane 0. 0. 1. "+str(bound_head+z_offset)+"\n"
|
|
1461
|
+
content_lipid += " end atoms\n"
|
|
1462
|
+
content_lipid += " atoms "+" ".join([t_atm for t_atm in self.parameters[lipid]["t_atm"].split(",")])+"\n"
|
|
1463
|
+
if leaflet == 0:
|
|
1464
|
+
if self.curvature is not None:
|
|
1465
|
+
content_lipid += " outside sphere 0. 0. "+str(-sphere_radius)+" "+str(sphere_radius-bound_tail+z_offset)+"\n"
|
|
1466
|
+
elif self.xygauss:
|
|
1467
|
+
content_lipid += " over xygauss 0. 0. "+self.xygauss[0]+" "+self.xygauss[1]+" "+str(-bound_tail+z_offset)+" "+self.xygauss[2]+"\n"
|
|
1468
|
+
else:
|
|
1469
|
+
content_lipid += " over plane 0. 0. 1. "+str(-bound_tail+z_offset)+"\n"
|
|
1470
|
+
else:
|
|
1471
|
+
if self.curvature is not None:
|
|
1472
|
+
content_lipid += " inside sphere 0. 0. "+str(-sphere_radius)+" "+str(sphere_radius+bound_tail+z_offset)+"\n"
|
|
1473
|
+
elif self.xygauss:
|
|
1474
|
+
content_lipid += " below xygauss 0. 0. "+self.xygauss[0]+" "+self.xygauss[1]+" "+str(bound_tail+z_offset)+" "+self.xygauss[2]+"\n"
|
|
1475
|
+
else:
|
|
1476
|
+
content_lipid += " below plane 0. 0. 1. "+str(bound_tail+z_offset)+"\n"
|
|
1477
|
+
content_lipid += " end atoms\n"
|
|
1478
|
+
content_lipid += "end structure\n\n"
|
|
1479
|
+
|
|
1480
|
+
######################## WATER && SOLUTE #####################################
|
|
1481
|
+
|
|
1482
|
+
for solvent in self.solvents.split(":"):
|
|
1483
|
+
solvent_pdb = solvent + ".pdb"
|
|
1484
|
+
solvent_path = self._out_path(solvent_pdb)
|
|
1485
|
+
if os.path.isfile(solvent_path):
|
|
1486
|
+
logger.info("Using " + solvent_path + " in the folder")
|
|
1487
|
+
else:
|
|
1488
|
+
try:
|
|
1489
|
+
shutil.copy(os.path.join(script_path, rep, "pdbs", solvent_pdb), solvent_path)
|
|
1490
|
+
except:
|
|
1491
|
+
pdbtar.extract(solvent_pdb, path=self.outdir)
|
|
1492
|
+
self.created_notrun.append(solvent_path)
|
|
1493
|
+
if not os.path.isfile(solvent_path):
|
|
1494
|
+
logger.critical("CRITICAL:" + solvent_path + " is not to found in the folder!")
|
|
1495
|
+
exit()
|
|
1496
|
+
|
|
1497
|
+
if self.curvature is not None:
|
|
1498
|
+
solvent_vol_up = sphere_integral_square(X_min,X_max,Y_min,Y_max,r1=sphere_radius+z_offset+leaflet_z,z_max=Z_dim[bilayer][1]+z_offset,c=-sphere_radius)-solvol[bilayer][1]
|
|
1499
|
+
solvent_vol_down = sphere_integral_square(X_min,X_max,Y_min,Y_max,z_min=Z_dim[bilayer][0]+z_offset,r2=sphere_radius+z_offset-leaflet_z,c=-sphere_radius)-solvol[bilayer][0]
|
|
1500
|
+
elif self.xygauss:
|
|
1501
|
+
solvent_vol_up = gauss_integral_square(X_min,X_max,Y_min,Y_max,*map(float,self.xygauss),g1=z_offset+leaflet_z,z_max=Z_dim[bilayer][1]+z_offset)-solvol[bilayer][1]
|
|
1502
|
+
solvent_vol_down = gauss_integral_square(X_min,X_max,Y_min,Y_max,*map(float,self.xygauss),z_min=Z_dim[bilayer][0]+z_offset,g2=z_offset-leaflet_z)-solvol[bilayer][0]
|
|
1503
|
+
else:
|
|
1504
|
+
solvent_vol_up = (X_len*Y_len*(abs(Z_dim[bilayer][1])-leaflet_z))-solvol[bilayer][1]
|
|
1505
|
+
solvent_vol_down = (X_len*Y_len*(abs(Z_dim[bilayer][0])-leaflet_z))-solvol[bilayer][0]
|
|
1506
|
+
if self.solvate:
|
|
1507
|
+
solvent_vol_up = solvent_vol_up+lipid_vol_up
|
|
1508
|
+
solvent_vol_down = solvent_vol_down+lipid_vol_down
|
|
1509
|
+
solvent_vol_tot = solvent_vol_up+solvent_vol_down
|
|
1510
|
+
if solvent_con is None:
|
|
1511
|
+
logger.critical(
|
|
1512
|
+
"CRITICAL:\n Martini mode enabled with CG solvent particles; solvent packing via PACKMOL requires solvent density/MW parameters.\n"
|
|
1513
|
+
" Use Insane4MemPrO (recommended for --martini) or choose an atomistic solvent from --available_solvents."
|
|
1514
|
+
)
|
|
1515
|
+
exit()
|
|
1516
|
+
watnum_up = int(solvent_vol_up*solvent_con)
|
|
1517
|
+
watnum_down = int(solvent_vol_down*solvent_con)
|
|
1518
|
+
|
|
1519
|
+
if self.solute is not None and not self.solute_inmem:
|
|
1520
|
+
for i,sol in enumerate(self.solute):
|
|
1521
|
+
logger.info("Adding "+self.solute_con[i]+" "+self.solute[i]+" to the water volume")
|
|
1522
|
+
grid_file, solute_vol = self.pdbvol(self.solute[i])
|
|
1523
|
+
self.created.append(grid_file)
|
|
1524
|
+
if self.solute_con[i].endswith("M") and is_number(self.solute_con[i][:-1]):
|
|
1525
|
+
solute_up = int(float(self.solute_con[i][:-1])*((solvent_vol_up*avogadro/(1*10**27))))
|
|
1526
|
+
solute_down = int(float(self.solute_con[i][:-1])*((solvent_vol_down*avogadro/(1*10**27))))
|
|
1527
|
+
elif self.solute_con[i].endswith("%") and is_number(self.solute_con[i][:-1]):
|
|
1528
|
+
solute_up = int(float(self.solute_con[i][:-1])/100*((solvent_vol_up/solute_vol)))
|
|
1529
|
+
solute_down = int(float(self.solute_con[i][:-1])/100*((solvent_vol_down/solute_vol)))
|
|
1530
|
+
solute_vol_up = int(float(self.solute_con[i][:-1])/100*(solvent_vol_up))
|
|
1531
|
+
solute_vol_down = int(float(self.solute_con[i][:-1])/100*(solvent_vol_down))
|
|
1532
|
+
elif is_number(self.solute_con[i]):
|
|
1533
|
+
try:
|
|
1534
|
+
int(self.solute_con[i])
|
|
1535
|
+
except:
|
|
1536
|
+
logger.error("ERROR:\n A number less than 1 is specified. If a concentration was intended, add M as a suffix!")
|
|
1537
|
+
exit()
|
|
1538
|
+
solute_up, solute_down = distribute_integer(int(self.solute_con[i]),[solvent_vol_up,solvent_vol_down])
|
|
1539
|
+
else:
|
|
1540
|
+
logger.error("ERROR:\n The format used to specify the number of molecules is not correct! It has to be a number or a number with an M/% suffix.")
|
|
1541
|
+
exit()
|
|
1542
|
+
if not self.solute_con[i].endswith("%"):
|
|
1543
|
+
solute_vol_up = solute_vol*solute_up
|
|
1544
|
+
solute_vol_down = solute_vol*solute_down
|
|
1545
|
+
watnum_up = watnum_up-int((solute_vol_up)*solvent_con)
|
|
1546
|
+
watnum_down = watnum_down-int((solute_vol_down)*solvent_con)
|
|
1547
|
+
if solute_up+solute_down < 1:
|
|
1548
|
+
logger.error("ERROR:\n The solute concentration is too low for the calculated box size. Try increasing the concentration.")
|
|
1549
|
+
exit()
|
|
1550
|
+
charge_solute += self.solute_charge[i]*(solute_down+solute_up)
|
|
1551
|
+
if not self.solvate and not self.self_assembly:
|
|
1552
|
+
if solute_down > 0:
|
|
1553
|
+
content_solute += "structure "+self.solute[i]+"\n"
|
|
1554
|
+
content_solute += " nloop "+str(self.nloop)+"\n"
|
|
1555
|
+
content_solute += " number "+str(solute_down)+"\n"
|
|
1556
|
+
if self.solute_prot_dist is not None and self.pdb[bilayer] != "None":
|
|
1557
|
+
content_solute += " outside cylinder 0. 0. "+str(minmax[2]-self.solute_prot_dist)+" 0. 0. 1. "+str(max_rad+self.solute_prot_dist)+" "+str(minmax[5]-minmax[2]+self.solute_prot_dist)+" \n"
|
|
1558
|
+
if self.curvature is not None:
|
|
1559
|
+
if not self.pbc:
|
|
1560
|
+
content_solute += " inside box "+str(round(X_min,2))+" "+str(round(Y_min,2))+" "+str(round(Z_dim[bilayer][0]+z_offset,2))+" "+str(round(X_max,2))+" "+str(round(Y_max,2))+" "+str(Z_dim[bilayer][1])+"\n"
|
|
1561
|
+
content_solute += " inside sphere 0. 0. "+str(-sphere_radius)+" "+str(sphere_radius-leaflet_z+z_offset)+"\n"
|
|
1562
|
+
elif self.xygauss:
|
|
1563
|
+
if not self.pbc:
|
|
1564
|
+
content_solute += " inside box "+str(round(X_min,2))+" "+str(round(Y_min,2))+" "+str(round(Z_dim[bilayer][0]+z_offset,2))+" "+str(round(X_max,2))+" "+str(round(Y_max,2))+" "+str(Z_dim[bilayer][1])+"\n"
|
|
1565
|
+
content_solute += " below xygauss 0. 0. "+self.xygauss[0]+" "+self.xygauss[1]+" "+str(-leaflet_z+z_offset)+" "+self.xygauss[2]+"\n"
|
|
1566
|
+
else:
|
|
1567
|
+
if self.pbc:
|
|
1568
|
+
content_solute += " below plane 0. 0. 1. "+str(-leaflet_z+z_offset)+"\n"
|
|
1569
|
+
else:
|
|
1570
|
+
content_solute += " inside box "+str(round(X_min,2))+" "+str(round(Y_min,2))+" "+str(round(Z_dim[bilayer][0]+z_offset,2))+" "+str(round(X_max,2))+" "+str(round(Y_max,2))+" "+str(-leaflet_z+z_offset)+"\n"
|
|
1571
|
+
content_solute += "end structure\n\n"
|
|
1572
|
+
if solute_up > 0:
|
|
1573
|
+
content_solute += "structure "+self.solute[i]+"\n"
|
|
1574
|
+
content_solute += " nloop "+str(self.nloop)+"\n"
|
|
1575
|
+
content_solute += " number "+str(solute_up)+"\n"
|
|
1576
|
+
if self.solute_prot_dist is not None and self.pdb[bilayer] != "None":
|
|
1577
|
+
content_solute += " outside cylinder 0. 0. "+str(minmax[2]-self.solute_prot_dist)+" 0. 0. 1. "+str(max_rad+self.solute_prot_dist)+" "+str(minmax[5]-minmax[2]+self.solute_prot_dist)+" \n"
|
|
1578
|
+
if self.curvature is not None:
|
|
1579
|
+
if not self.pbc:
|
|
1580
|
+
content_solute += " inside box "+str(round(X_min,2))+" "+str(round(Y_min,2))+" "+str(round(Z_dim[bilayer][0]+z_offset,2))+" "+str(round(X_max,2))+" "+str(round(Y_max,2))+" "+str(Z_dim[bilayer][1])+"\n"
|
|
1581
|
+
content_solute += " outside sphere 0. 0. "+str(-sphere_radius)+" "+str(sphere_radius+leaflet_z+z_offset)+"\n"
|
|
1582
|
+
elif self.xygauss:
|
|
1583
|
+
if not self.pbc:
|
|
1584
|
+
content_solute += " inside box "+str(round(X_min,2))+" "+str(round(Y_min,2))+" "+str(round(Z_dim[bilayer][0]+z_offset,2))+" "+str(round(X_max,2))+" "+str(round(Y_max,2))+" "+str(Z_dim[bilayer][1])+"\n"
|
|
1585
|
+
content_solute += " below xygauss 0. 0. "+self.xygauss[0]+" "+self.xygauss[1]+" "+str(leaflet_z+z_offset)+" "+self.xygauss[2]+"\n"
|
|
1586
|
+
else:
|
|
1587
|
+
if self.pbc:
|
|
1588
|
+
content_solute += " above plane 0. 0. 1. "+str(leaflet_z+z_offset)+"\n"
|
|
1589
|
+
else:
|
|
1590
|
+
content_solute += " inside box "+str(round(X_min,2))+" "+str(round(Y_min,2))+" "+str(leaflet_z+z_offset)+" "+str(round(X_max,2))+" "+str(round(Y_max,2))+" "+str(round(Z_dim[bilayer][1]+z_offset,2))+"\n"
|
|
1591
|
+
content_solute += "end structure\n\n"
|
|
1592
|
+
else:
|
|
1593
|
+
content_solute += "structure "+self.solute[i]+"\n"
|
|
1594
|
+
content_solute += " nloop "+str(self.nloop)+"\n"
|
|
1595
|
+
content_solute += " number "+str(solute_down+solute_up)+"\n"
|
|
1596
|
+
if self.solute_prot_dist is not None and self.pdb[bilayer] != "None":
|
|
1597
|
+
content_solute += " outside cylinder 0. 0. "+str(minmax[2]-self.solute_prot_dist)+" 0. 0. 1. "+str(max_rad+self.solute_prot_dist)+" "+str(minmax[5]-minmax[2]+self.solute_prot_dist)+" \n"
|
|
1598
|
+
if not self.pbc:
|
|
1599
|
+
content_solute += " inside box "+str(round(X_min,2))+" "+str(round(Y_min,2))+" "+str(round(Z_dim[bilayer][0]+z_offset,2))+" "+str(round(X_max,2))+" "+str(round(Y_max,2))+" "+str(round(Z_dim[bilayer][1]+z_offset,2))+"\n"
|
|
1600
|
+
content_solute += "end structure\n\n"
|
|
1601
|
+
|
|
1602
|
+
elif (self.solute is not None or self.solute_con is not None) and not self.solute_inmem:
|
|
1603
|
+
logger.error("ERROR:\n The solute parameters are incomplete. Please include both a solute pdb and a concentration.")
|
|
1604
|
+
exit()
|
|
1605
|
+
|
|
1606
|
+
################################# SALT & CHARGES #############################
|
|
1607
|
+
|
|
1608
|
+
pos_up = pos_down = 0
|
|
1609
|
+
neg_up = neg_down = 0
|
|
1610
|
+
charge = charges[bilayer]
|
|
1611
|
+
if not self.solvate:
|
|
1612
|
+
for leaflet in composition[bilayer]:
|
|
1613
|
+
for lipid in composition[bilayer][leaflet]:
|
|
1614
|
+
charge_lip += int(round(composition[bilayer][leaflet][lipid]*lipnum_dict[bilayer][leaflet]))*int(self.parameters[lipid]["charge"])
|
|
1615
|
+
if charge_lip != 0:
|
|
1616
|
+
logger.debug("The lipids contribute a charge of "+str(charge_lip)+" to the system. It will be considered for the neutralization.")
|
|
1617
|
+
charge += charge_lip
|
|
1618
|
+
if charge_solute != 0:
|
|
1619
|
+
logger.debug("The solutes contribute a charge of "+str(charge_solute)+" to the system. It will be considered for the neutralization.")
|
|
1620
|
+
charge += charge_solute
|
|
1621
|
+
if charge > 0:
|
|
1622
|
+
neg_up = int(round(charge*(solvent_vol_up/(solvent_vol_tot))))
|
|
1623
|
+
neg_down = int(round(charge*(solvent_vol_down/(solvent_vol_tot))))
|
|
1624
|
+
else:
|
|
1625
|
+
pos_up = int(round((abs(charge)/self.ion_dict[self.salt_c][1])*(solvent_vol_up/(solvent_vol_tot))))
|
|
1626
|
+
pos_down = int(round((abs(charge)/self.ion_dict[self.salt_c][1])*(solvent_vol_down/(solvent_vol_tot))))
|
|
1627
|
+
con_pos = (pos_up+pos_down)/((solvent_vol_tot)*avogadro/(1*10**27))
|
|
1628
|
+
con_neg = (neg_up+neg_down)/((solvent_vol_tot)*avogadro/(1*10**27))
|
|
1629
|
+
if self.salt:
|
|
1630
|
+
if con_pos > saltcon or con_neg > saltcon*self.ion_dict[self.salt_c][1]:
|
|
1631
|
+
# print pos_up, pos_down, neg_up, neg_down, saltnum_up, saltnum_down
|
|
1632
|
+
logger.warning("""WARNING:
|
|
1633
|
+
The concentration of ions required to neutralize the system is higher than the concentration specified.
|
|
1634
|
+
Either increase the salt concentration by using the --saltcon flag or run the script without the --salt flag.""")
|
|
1635
|
+
logger.info("Positive ion concentration: "+str(round(con_pos,3)))
|
|
1636
|
+
logger.info("Negative ion concentration: "+str(round(con_neg,3)))
|
|
1637
|
+
logger.info("Salt concentration specified: "+str(saltcon))
|
|
1638
|
+
if override_salt:
|
|
1639
|
+
if self.verbose:
|
|
1640
|
+
logger.info("Overriding salt concentration...")
|
|
1641
|
+
saltcon = max(con_pos,con_neg)
|
|
1642
|
+
pass
|
|
1643
|
+
else:
|
|
1644
|
+
exit()
|
|
1645
|
+
saltnum_up = int(solvent_vol_up*saltcon*avogadro/(1*10**27))
|
|
1646
|
+
saltnum_down = int(solvent_vol_down*saltcon*avogadro/(1*10**27))
|
|
1647
|
+
|
|
1648
|
+
if abs(charge)/2 < min(saltnum_up,saltnum_down):
|
|
1649
|
+
pos_up += saltnum_up-abs(charge)/(self.ion_dict[self.salt_c][1]*2)
|
|
1650
|
+
pos_down += saltnum_down-abs(charge)/(self.ion_dict[self.salt_c][1]*2)
|
|
1651
|
+
neg_up += saltnum_up*self.ion_dict[self.salt_c][1]-abs(charge)/2
|
|
1652
|
+
neg_down += saltnum_down*self.ion_dict[self.salt_c][1]-abs(charge)/2
|
|
1653
|
+
|
|
1654
|
+
if self.charge_imbalance != 0:
|
|
1655
|
+
if n % 2 == 0:
|
|
1656
|
+
if self.imbalance_ion == "cat":
|
|
1657
|
+
pos_up += self.charge_imbalance/self.ion_dict[self.salt_c][1]
|
|
1658
|
+
pos_down -= self.charge_imbalance/self.ion_dict[self.salt_c][1]
|
|
1659
|
+
else:
|
|
1660
|
+
neg_up -= self.charge_imbalance
|
|
1661
|
+
neg_down += self.charge_imbalance
|
|
1662
|
+
else:
|
|
1663
|
+
if self.imbalance_ion == "cat":
|
|
1664
|
+
pos_up -= self.charge_imbalance/self.ion_dict[self.salt_c][1]
|
|
1665
|
+
pos_down += self.charge_imbalance/self.ion_dict[self.salt_c][1]
|
|
1666
|
+
else:
|
|
1667
|
+
neg_up += self.charge_imbalance
|
|
1668
|
+
neg_down -= self.charge_imbalance
|
|
1669
|
+
|
|
1670
|
+
charge_data= """
|
|
1671
|
+
Salt and charge info:
|
|
1672
|
+
Upper positive charges = %-9s
|
|
1673
|
+
Lower positive charges = %-9s
|
|
1674
|
+
Upper negative charges = %-9s
|
|
1675
|
+
Lower negative charges = %-9s
|
|
1676
|
+
Charge imbalance = %-9s
|
|
1677
|
+
Upper salt number = %-9s
|
|
1678
|
+
Lower salt number = %-9s
|
|
1679
|
+
"""
|
|
1680
|
+
logger.debug(charge_data % ( pos_up, pos_down, neg_up, neg_down, self.charge_imbalance, saltnum_up, saltnum_down ))
|
|
1681
|
+
|
|
1682
|
+
checkup = [pos_up,pos_down,neg_up,neg_down]
|
|
1683
|
+
if any(v < 0 for v in checkup):
|
|
1684
|
+
logger.critical("CRITICAL:\n The applied charge imbalance caused a negative number of ions! Check your input")
|
|
1685
|
+
exit()
|
|
1686
|
+
|
|
1687
|
+
|
|
1688
|
+
# new_con_neg = (neg_up+neg_down)/(((solvent_vol_tot)*avogadro/(1*10**27)))
|
|
1689
|
+
# new_con_pos = (pos_up+pos_down)/(((solvent_vol_tot)*avogadro/(1*10**27)))
|
|
1690
|
+
|
|
1691
|
+
cation_path = self._out_path(cation + ".pdb")
|
|
1692
|
+
anion_path = self._out_path(anion + ".pdb")
|
|
1693
|
+
try:
|
|
1694
|
+
shutil.copy(os.path.join(script_path, rep, "pdbs", cation + ".pdb"), cation_path)
|
|
1695
|
+
except:
|
|
1696
|
+
pdbtar.extract(cation + ".pdb", path=self.outdir)
|
|
1697
|
+
self.created_notrun.append(cation_path)
|
|
1698
|
+
try:
|
|
1699
|
+
shutil.copy(os.path.join(script_path, rep, "pdbs", anion + ".pdb"), anion_path)
|
|
1700
|
+
except:
|
|
1701
|
+
pdbtar.extract(anion + ".pdb", path=self.outdir)
|
|
1702
|
+
self.created_notrun.append(anion_path)
|
|
1703
|
+
if not self.solvate and not self.self_assembly:
|
|
1704
|
+
if pos_down > 0:
|
|
1705
|
+
content_ion += "structure " + self._packmol_path(cation_path) + "\n"
|
|
1706
|
+
content_ion += " nloop "+str(self.nloop)+"\n"
|
|
1707
|
+
content_ion += " number "+str(int(pos_down))+"\n"
|
|
1708
|
+
if self.curvature is not None:
|
|
1709
|
+
if not self.pbc:
|
|
1710
|
+
content_ion += " inside box "+str(round(X_min,2))+" "+str(round(Y_min,2))+" "+str(round(Z_dim[bilayer][0]+z_offset,2))+" "+str(round(X_max,2))+" "+str(round(Y_max,2))+" "+str(Z_dim[bilayer][1])+"\n"
|
|
1711
|
+
content_ion += " inside sphere 0. 0. "+str(-sphere_radius)+" "+str(sphere_radius-leaflet_z+z_offset)+"\n"
|
|
1712
|
+
elif self.xygauss:
|
|
1713
|
+
if not self.pbc:
|
|
1714
|
+
content_ion += " inside box "+str(round(X_min,2))+" "+str(round(Y_min,2))+" "+str(round(Z_dim[bilayer][0]+z_offset,2))+" "+str(round(X_max,2))+" "+str(round(Y_max,2))+" "+str(Z_dim[bilayer][1])+"\n"
|
|
1715
|
+
content_ion += " below xygauss 0. 0. "+self.xygauss[0]+" "+self.xygauss[1]+" "+str(-leaflet_z+z_offset)+" "+self.xygauss[2]+"\n"
|
|
1716
|
+
else:
|
|
1717
|
+
if self.pbc:
|
|
1718
|
+
content_ion += " below plane 0. 0. 1. "+str(-leaflet_z+z_offset)+"\n"
|
|
1719
|
+
if bilayer > 0:
|
|
1720
|
+
content_ion += " above plane 0. 0. 1. "+str(round(Z_dim[bilayer][0]+z_offset,2))+"\n"
|
|
1721
|
+
else:
|
|
1722
|
+
content_ion += " inside box "+str(round(X_min,2))+" "+str(round(Y_min,2))+" "+str(round(Z_dim[bilayer][0]+z_offset,2))+" "+str(round(X_max,2))+" "+str(round(Y_max,2))+" "+str(-leaflet_z+z_offset)+"\n"
|
|
1723
|
+
content_ion += "end structure\n\n"
|
|
1724
|
+
if pos_up > 0:
|
|
1725
|
+
content_ion += "structure " + self._packmol_path(cation_path) + "\n"
|
|
1726
|
+
content_ion += " nloop "+str(self.nloop)+"\n"
|
|
1727
|
+
content_ion += " number "+str(int(pos_up))+"\n"
|
|
1728
|
+
if self.curvature is not None:
|
|
1729
|
+
if not self.pbc:
|
|
1730
|
+
content_ion += " inside box "+str(round(X_min,2))+" "+str(round(Y_min,2))+" "+str(round(Z_dim[bilayer][0]+z_offset,2))+" "+str(round(X_max,2))+" "+str(round(Y_max,2))+" "+str(Z_dim[bilayer][1])+"\n"
|
|
1731
|
+
content_ion += " outside sphere 0. 0. "+str(-sphere_radius)+" "+str(sphere_radius+leaflet_z+z_offset)+"\n"
|
|
1732
|
+
elif self.xygauss:
|
|
1733
|
+
if not self.pbc:
|
|
1734
|
+
content_ion += " inside box "+str(round(X_min,2))+" "+str(round(Y_min,2))+" "+str(round(Z_dim[bilayer][0]+z_offset,2))+" "+str(round(X_max,2))+" "+str(round(Y_max,2))+" "+str(Z_dim[bilayer][1])+"\n"
|
|
1735
|
+
content_ion += " over xygauss 0. 0. "+self.xygauss[0]+" "+self.xygauss[1]+" "+str(leaflet_z+z_offset)+" "+self.xygauss[2]+"\n"
|
|
1736
|
+
else:
|
|
1737
|
+
if self.pbc:
|
|
1738
|
+
content_ion += " above plane 0. 0. 1. "+str(leaflet_z+z_offset)+"\n"
|
|
1739
|
+
if bilayer+1 < len(composition):
|
|
1740
|
+
content_ion += " below plane 0. 0. 1. "+str(round(Z_dim[bilayer][1]+z_offset,2))+"\n"
|
|
1741
|
+
else:
|
|
1742
|
+
content_ion += " inside box "+str(round(X_min,2))+" "+str(round(Y_min,2))+" "+str(leaflet_z+z_offset)+" "+str(round(X_max,2))+" "+str(round(Y_max,2))+" "+str(round(Z_dim[bilayer][1]+z_offset,2))+"\n"
|
|
1743
|
+
content_ion += "end structure\n\n"
|
|
1744
|
+
if neg_down > 0:
|
|
1745
|
+
content_ion += "structure " + self._packmol_path(anion_path) + "\n"
|
|
1746
|
+
content_ion += " nloop "+str(self.nloop)+"\n"
|
|
1747
|
+
content_ion += " number "+str(int(neg_down))+"\n"
|
|
1748
|
+
if self.curvature is not None:
|
|
1749
|
+
if not self.pbc:
|
|
1750
|
+
content_ion += " inside box "+str(round(X_min,2))+" "+str(round(Y_min,2))+" "+str(round(Z_dim[bilayer][0]+z_offset,2))+" "+str(round(X_max,2))+" "+str(round(Y_max,2))+" "+str(Z_dim[bilayer][1])+"\n"
|
|
1751
|
+
content_ion += " inside sphere 0. 0. "+str(-sphere_radius)+" "+str(sphere_radius-leaflet_z+z_offset)+"\n"
|
|
1752
|
+
elif self.xygauss:
|
|
1753
|
+
if not self.pbc:
|
|
1754
|
+
content_ion += " inside box "+str(round(X_min,2))+" "+str(round(Y_min,2))+" "+str(round(Z_dim[bilayer][0]+z_offset,2))+" "+str(round(X_max,2))+" "+str(round(Y_max,2))+" "+str(Z_dim[bilayer][1])+"\n"
|
|
1755
|
+
content_ion += " below xygauss 0. 0. "+self.xygauss[0]+" "+self.xygauss[1]+" "+str(-leaflet_z+z_offset)+" "+self.xygauss[2]+"\n"
|
|
1756
|
+
else:
|
|
1757
|
+
if self.pbc:
|
|
1758
|
+
content_ion += " below plane 0. 0. 1. "+str(-leaflet_z+z_offset)+"\n"
|
|
1759
|
+
if bilayer > 0:
|
|
1760
|
+
content_ion += " above plane 0. 0. 1. "+str(round(Z_dim[bilayer][0]+z_offset,2))+"\n"
|
|
1761
|
+
else:
|
|
1762
|
+
content_ion += " inside box "+str(round(X_min,2))+" "+str(round(Y_min,2))+" "+str(round(Z_dim[bilayer][0]+z_offset,2))+" "+str(round(X_max,2))+" "+str(round(Y_max,2))+" "+str(-leaflet_z+z_offset)+"\n"
|
|
1763
|
+
content_ion += "end structure\n\n"
|
|
1764
|
+
if neg_up > 0:
|
|
1765
|
+
content_ion += "structure " + self._packmol_path(anion_path) + "\n"
|
|
1766
|
+
content_ion += " nloop "+str(self.nloop)+"\n"
|
|
1767
|
+
content_ion += " number "+str(int(neg_up))+"\n"
|
|
1768
|
+
if self.curvature is not None:
|
|
1769
|
+
if not self.pbc:
|
|
1770
|
+
content_ion += " inside box "+str(round(X_min,2))+" "+str(round(Y_min,2))+" "+str(round(Z_dim[bilayer][0]+z_offset,2))+" "+str(round(X_max,2))+" "+str(round(Y_max,2))+" "+str(Z_dim[bilayer][1])+"\n"
|
|
1771
|
+
content_ion += " outside sphere 0. 0. "+str(-sphere_radius)+" "+str(sphere_radius+leaflet_z+z_offset)+"\n"
|
|
1772
|
+
elif self.xygauss:
|
|
1773
|
+
if not self.pbc:
|
|
1774
|
+
content_ion += " inside box "+str(round(X_min,2))+" "+str(round(Y_min,2))+" "+str(round(Z_dim[bilayer][0]+z_offset,2))+" "+str(round(X_max,2))+" "+str(round(Y_max,2))+" "+str(Z_dim[bilayer][1])+"\n"
|
|
1775
|
+
content_ion += " over xygauss 0. 0. "+self.xygauss[0]+" "+self.xygauss[1]+" "+str(leaflet_z+z_offset)+" "+self.xygauss[2]+"\n"
|
|
1776
|
+
else:
|
|
1777
|
+
if self.pbc:
|
|
1778
|
+
content_ion += " above plane 0. 0. 1. "+str(leaflet_z+z_offset)+"\n"
|
|
1779
|
+
if bilayer+1 < len(composition):
|
|
1780
|
+
content_ion += " below plane 0. 0. 1. "+str(round(Z_dim[bilayer][1]+z_offset,2))+"\n"
|
|
1781
|
+
else:
|
|
1782
|
+
content_ion += " inside box "+str(round(X_min,2))+" "+str(round(Y_min,2))+" "+str(leaflet_z+z_offset)+" "+str(round(X_max,2))+" "+str(round(Y_max,2))+" "+str(round(Z_dim[bilayer][1]+z_offset,2))+"\n"
|
|
1783
|
+
content_ion += "end structure\n\n"
|
|
1784
|
+
else:
|
|
1785
|
+
if pos_down+pos_up > 0:
|
|
1786
|
+
content_ion += "structure " + self._packmol_path(cation_path) + "\n"
|
|
1787
|
+
content_ion += " nloop "+str(self.nloop)+"\n"
|
|
1788
|
+
content_ion += " number "+str(int(pos_down+pos_up))+"\n"
|
|
1789
|
+
if not self.pbc:
|
|
1790
|
+
content_ion += " inside box "+str(round(X_min,2))+" "+str(round(Y_min,2))+" "+str(round(Z_dim[bilayer][0]+z_offset,2))+" "+str(round(X_max,2))+" "+str(round(Y_max,2))+" "+str(round(Z_dim[bilayer][1]+z_offset,2))+"\n"
|
|
1791
|
+
content_ion += "end structure\n\n"
|
|
1792
|
+
if neg_down+neg_up > 0:
|
|
1793
|
+
content_ion += "structure " + self._packmol_path(anion_path) + "\n"
|
|
1794
|
+
content_ion += " nloop "+str(self.nloop)+"\n"
|
|
1795
|
+
content_ion += " number "+str(int(neg_down+neg_up))+"\n"
|
|
1796
|
+
if not self.pbc:
|
|
1797
|
+
content_ion += " inside box "+str(round(X_min,2))+" "+str(round(Y_min,2))+" "+str(round(Z_dim[bilayer][0]+z_offset,2))+" "+str(round(X_max,2))+" "+str(round(Y_max,2))+" "+str(round(Z_dim[bilayer][1]+z_offset,2))+"\n"
|
|
1798
|
+
content_ion += "end structure\n\n"
|
|
1799
|
+
|
|
1800
|
+
for i, solvent in enumerate(self.solvents.split(":")):
|
|
1801
|
+
solvent_pdb = solvent + ".pdb"
|
|
1802
|
+
solvent_path = self._out_path(solvent_pdb)
|
|
1803
|
+
solvents_up = int(
|
|
1804
|
+
(watnum_up/solvent_con)
|
|
1805
|
+
* (solvent_ratios[i]/float(sum(solvent_ratios)))
|
|
1806
|
+
* (avogadro/(1*10**24))
|
|
1807
|
+
* (float(self.sparameters[solvent]["density"])/float(self.sparameters[solvent]["MW"]))
|
|
1808
|
+
)
|
|
1809
|
+
solvents_down = int(
|
|
1810
|
+
(watnum_down/solvent_con)
|
|
1811
|
+
* (solvent_ratios[i]/float(sum(solvent_ratios)))
|
|
1812
|
+
* (avogadro/(1*10**24))
|
|
1813
|
+
* (float(self.sparameters[solvent]["density"])/float(self.sparameters[solvent]["MW"]))
|
|
1814
|
+
)
|
|
1815
|
+
|
|
1816
|
+
content_solvent_header = "structure " + self._packmol_path(solvent_path) + "\n"
|
|
1817
|
+
if self.sirah:
|
|
1818
|
+
content_solvent_header += " atoms 5\n"
|
|
1819
|
+
content_solvent_header += " radius 2.5\n"
|
|
1820
|
+
content_solvent_header += " end atoms\n"
|
|
1821
|
+
elif solvent == "WAT" and self.watorient:
|
|
1822
|
+
content_solvent_header += " atoms 1\n"
|
|
1823
|
+
content_solvent_header += " radius 1.5\n"
|
|
1824
|
+
content_solvent_header += " end atoms\n"
|
|
1825
|
+
content_solvent_header += " nloop "+str(self.nloop)+"\n"
|
|
1826
|
+
|
|
1827
|
+
if not self.solvate and not self.self_assembly:
|
|
1828
|
+
if solvents_down > 0:
|
|
1829
|
+
content_solvent += content_solvent_header
|
|
1830
|
+
content_solvent += " number "+str(solvents_down)+"\n" # deleted -pos_down-neg_down. Was it necessary?
|
|
1831
|
+
if self.curvature is not None:
|
|
1832
|
+
if not self.pbc:
|
|
1833
|
+
content_solvent += " inside box "+str(round(X_min,2))+" "+str(round(Y_min,2))+" "+str(round(Z_dim[bilayer][0]+z_offset,2))+" "+str(round(X_max,2))+" "+str(round(Y_max,2))+" "+str(Z_dim[bilayer][1])+"\n"
|
|
1834
|
+
content_solvent += " inside sphere 0. 0. "+str(-sphere_radius)+" "+str(sphere_radius-leaflet_z+z_offset)+"\n"
|
|
1835
|
+
elif self.xygauss:
|
|
1836
|
+
if not self.pbc:
|
|
1837
|
+
content_solvent += " inside box "+str(round(X_min,2))+" "+str(round(Y_min,2))+" "+str(round(Z_dim[bilayer][0]+z_offset,2))+" "+str(round(X_max,2))+" "+str(round(Y_max,2))+" "+str(Z_dim[bilayer][1])+"\n"
|
|
1838
|
+
content_solvent += " below xygauss 0. 0. "+self.xygauss[0]+" "+self.xygauss[1]+" "+str(-leaflet_z+z_offset)+" "+self.xygauss[2]+"\n"
|
|
1839
|
+
else:
|
|
1840
|
+
if self.pbc:
|
|
1841
|
+
content_solvent += " below plane 0. 0. 1. "+str(-leaflet_z+z_offset)+"\n"
|
|
1842
|
+
if bilayer > 0:
|
|
1843
|
+
content_solvent += " above plane 0. 0. 1. "+str(round(Z_dim[bilayer][0]+z_offset,2))+"\n"
|
|
1844
|
+
else:
|
|
1845
|
+
content_solvent += " inside box "+str(round(X_min,2))+" "+str(round(Y_min,2))+" "+str(round(Z_dim[bilayer][0]+z_offset,2))+" "+str(round(X_max,2))+" "+str(round(Y_max,2))+" "+str(-leaflet_z+z_offset)+"\n"
|
|
1846
|
+
content_solvent += "end structure\n\n"
|
|
1847
|
+
if solvents_up > 0:
|
|
1848
|
+
content_solvent += content_solvent_header
|
|
1849
|
+
content_solvent += " number "+str(solvents_up)+"\n" # deleted -pos_up-neg_up. Was it necessary?
|
|
1850
|
+
if self.curvature is not None:
|
|
1851
|
+
if not self.pbc:
|
|
1852
|
+
content_solvent += " inside box "+str(round(X_min,2))+" "+str(round(Y_min,2))+" "+str(round(Z_dim[bilayer][0]+z_offset,2))+" "+str(round(X_max,2))+" "+str(round(Y_max,2))+" "+str(Z_dim[bilayer][1])+"\n"
|
|
1853
|
+
content_solvent += " outside sphere 0. 0. "+str(-sphere_radius)+" "+str(sphere_radius+leaflet_z+z_offset)+"\n"
|
|
1854
|
+
elif self.xygauss:
|
|
1855
|
+
if not self.pbc:
|
|
1856
|
+
content_solvent += " inside box "+str(round(X_min,2))+" "+str(round(Y_min,2))+" "+str(round(Z_dim[bilayer][0]+z_offset,2))+" "+str(round(X_max,2))+" "+str(round(Y_max,2))+" "+str(Z_dim[bilayer][1])+"\n"
|
|
1857
|
+
content_solvent += " over xygauss 0. 0. "+self.xygauss[0]+" "+self.xygauss[1]+" "+str(leaflet_z+z_offset)+" "+self.xygauss[2]+"\n"
|
|
1858
|
+
else:
|
|
1859
|
+
if self.pbc:
|
|
1860
|
+
content_solvent += " above plane 0. 0. 1. "+str(leaflet_z+z_offset)+"\n"
|
|
1861
|
+
if bilayer+1 < len(composition):
|
|
1862
|
+
content_solvent += " below plane 0. 0. 1. "+str(round(Z_dim[bilayer][1]+z_offset,2))+"\n"
|
|
1863
|
+
else:
|
|
1864
|
+
content_solvent += " inside box "+str(round(X_min,2))+" "+str(round(Y_min,2))+" "+str(leaflet_z+z_offset)+" "+str(round(X_max,2))+" "+str(round(Y_max,2))+" "+str(round(Z_dim[bilayer][1]+z_offset,2))+"\n"
|
|
1865
|
+
content_solvent += "end structure\n\n"
|
|
1866
|
+
elif solvents_down+solvents_up > 0:
|
|
1867
|
+
content_solvent += content_solvent_header
|
|
1868
|
+
content_solvent += " number "+str(solvents_down+solvents_up)+"\n" # deleted -pos-neg. Was it necessary?
|
|
1869
|
+
if not self.pbc:
|
|
1870
|
+
content_solvent += " inside box "+str(round(X_min,2))+" "+str(round(Y_min,2))+" "+str(round(Z_dim[bilayer][0]+z_offset,2))+" "+str(round(X_max,2))+" "+str(round(Y_max,2))+" "+str(round(Z_dim[bilayer][1]+z_offset,2))+"\n"
|
|
1871
|
+
content_solvent += "end structure\n\n"
|
|
1872
|
+
|
|
1873
|
+
if self.nocounter:
|
|
1874
|
+
logger.info("Ions will not be added. System charge will be handled downstream.")
|
|
1875
|
+
content_ion = ""
|
|
1876
|
+
self.contents = content_header+content_prot+content_lipid+content_solvent+content_ion+content_solute
|
|
1877
|
+
|
|
1878
|
+
if not self.solvate:
|
|
1879
|
+
box_data = """
|
|
1880
|
+
Lower water box vol = %-9s A^3
|
|
1881
|
+
Upper water box vol = %-9s A^3
|
|
1882
|
+
Mem. upper leaflet vol = %-9s A^3
|
|
1883
|
+
Mem. lower leaflet vol = %-9s A^3
|
|
1884
|
+
|
|
1885
|
+
"""
|
|
1886
|
+
|
|
1887
|
+
logger.debug(box_data % (round(solvent_vol_down,2), round(solvent_vol_up,2), round(lipid_vol_up,2), round(lipid_vol_down,2)))
|
|
1888
|
+
else:
|
|
1889
|
+
box_data = """
|
|
1890
|
+
Water box vol = %-9s A^3
|
|
1891
|
+
|
|
1892
|
+
"""
|
|
1893
|
+
|
|
1894
|
+
logger.debug(box_data % (round(solvent_vol_down+solvent_vol_up,2)))
|
|
1895
|
+
|
|
1896
|
+
return True
|
|
1897
|
+
|
|
1898
|
+
|
|
1899
|
+
|
|
1900
|
+
############## SET OF USED FUNCTIONS #####################
|
|
1901
|
+
def _finalize_martini_build(self):
|
|
1902
|
+
if not self.martini_build_output:
|
|
1903
|
+
logger.critical("CRITICAL:\n Martini build output not found. Check MemPrO/Insane4MemPrO outputs.")
|
|
1904
|
+
exit()
|
|
1905
|
+
cg_dir = self.martini_build_output
|
|
1906
|
+
src_gro = sorted(glob.glob(os.path.join(cg_dir, "*.gro")))
|
|
1907
|
+
src_top = sorted(glob.glob(os.path.join(cg_dir, "*.top")))
|
|
1908
|
+
src_pdb = sorted(glob.glob(os.path.join(cg_dir, "*.pdb")))
|
|
1909
|
+
src_itp = sorted(glob.glob(os.path.join(cg_dir, "molecule_*.itp")))
|
|
1910
|
+
if not (src_gro or src_top or src_pdb):
|
|
1911
|
+
logger.critical("CRITICAL:\n No CG outputs found in %s.", cg_dir)
|
|
1912
|
+
exit()
|
|
1913
|
+
base = Path(self.outfile).stem if self.outfile else "cg_system"
|
|
1914
|
+
dest_dir = os.path.dirname(os.path.abspath(self.outfile)) if self.outfile else self.outdir
|
|
1915
|
+
dest_map = {
|
|
1916
|
+
".gro": src_gro[:1],
|
|
1917
|
+
".top": src_top[:1],
|
|
1918
|
+
".pdb": src_pdb[:1],
|
|
1919
|
+
}
|
|
1920
|
+
written = []
|
|
1921
|
+
for ext, candidates in dest_map.items():
|
|
1922
|
+
if not candidates:
|
|
1923
|
+
continue
|
|
1924
|
+
src = candidates[0]
|
|
1925
|
+
dest = os.path.join(dest_dir, base + ext)
|
|
1926
|
+
if os.path.exists(dest) and not self.overwrite:
|
|
1927
|
+
logger.warning("Output exists (use --overwrite to replace): %s", dest)
|
|
1928
|
+
written.append(dest)
|
|
1929
|
+
continue
|
|
1930
|
+
shutil.copy(src, dest)
|
|
1931
|
+
written.append(dest)
|
|
1932
|
+
itp_in_output = sorted(glob.glob(os.path.join(dest_dir, "molecule_*.itp")))
|
|
1933
|
+
if not written:
|
|
1934
|
+
logger.critical("CRITICAL:\n No CG outputs were written to %s.", dest_dir)
|
|
1935
|
+
exit()
|
|
1936
|
+
if src_top:
|
|
1937
|
+
top_path = os.path.join(dest_dir, base + ".top")
|
|
1938
|
+
fallback_top = os.path.join(dest_dir, "topol.top")
|
|
1939
|
+
if not os.path.exists(top_path) and os.path.exists(fallback_top):
|
|
1940
|
+
top_path = fallback_top
|
|
1941
|
+
source_top = None
|
|
1942
|
+
martinize_top = sorted(glob.glob(os.path.join(dest_dir, "*_topol.top")))
|
|
1943
|
+
if martinize_top:
|
|
1944
|
+
source_top = martinize_top[0]
|
|
1945
|
+
self._postprocess_martini_topology(
|
|
1946
|
+
cg_dir=cg_dir,
|
|
1947
|
+
top_path=top_path,
|
|
1948
|
+
itp_paths=itp_in_output,
|
|
1949
|
+
source_top=source_top,
|
|
1950
|
+
)
|
|
1951
|
+
logger.info("Martini build complete. Using Insane4MemPrO outputs as final output:")
|
|
1952
|
+
for path in written:
|
|
1953
|
+
logger.info(" %s", path)
|
|
1954
|
+
print("DONE!")
|
|
1955
|
+
sys.exit(0)
|
|
1956
|
+
|
|
1957
|
+
def _auto_build_insane_args(self):
|
|
1958
|
+
if not self.lipids or len(self.lipids) > 2:
|
|
1959
|
+
return None
|
|
1960
|
+
if self.martini and self.solvents:
|
|
1961
|
+
insane_solvents = self._load_insane_solvents()
|
|
1962
|
+
if insane_solvents:
|
|
1963
|
+
insane_solvents = {s.upper() for s in insane_solvents}
|
|
1964
|
+
missing = []
|
|
1965
|
+
for solvent in self.solvents.split(":"):
|
|
1966
|
+
token = solvent.strip()
|
|
1967
|
+
if token.upper() == "WAT":
|
|
1968
|
+
continue
|
|
1969
|
+
if token and token.upper() not in insane_solvents:
|
|
1970
|
+
missing.append(token)
|
|
1971
|
+
if missing:
|
|
1972
|
+
logger.critical(
|
|
1973
|
+
"CRITICAL:\n Martini solvents not found in Insane4MemPrO solventParticles: %s",
|
|
1974
|
+
", ".join(sorted(set(missing))),
|
|
1975
|
+
)
|
|
1976
|
+
exit()
|
|
1977
|
+
else:
|
|
1978
|
+
logger.warning(
|
|
1979
|
+
"WARNING:\n Insane4MemPrO solvents list not available; skipping Martini solvent validation."
|
|
1980
|
+
)
|
|
1981
|
+
args = []
|
|
1982
|
+
ratio_list = []
|
|
1983
|
+
if self.ratio:
|
|
1984
|
+
ratio_list = list(self.ratio)
|
|
1985
|
+
if len(ratio_list) != len(self.lipids):
|
|
1986
|
+
return None
|
|
1987
|
+
else:
|
|
1988
|
+
ratio_list = [""] * len(self.lipids)
|
|
1989
|
+
for index, (lipid_spec, ratio_spec) in enumerate(zip(self.lipids, ratio_list)):
|
|
1990
|
+
if "//" in lipid_spec:
|
|
1991
|
+
if ratio_spec and "//" not in ratio_spec:
|
|
1992
|
+
return None
|
|
1993
|
+
lipid_parts = lipid_spec.split("//")
|
|
1994
|
+
if len(lipid_parts) != 2:
|
|
1995
|
+
return None
|
|
1996
|
+
ratio_parts = ratio_spec.split("//") if ratio_spec else ["", ""]
|
|
1997
|
+
if ratio_spec and len(ratio_parts) != 2:
|
|
1998
|
+
return None
|
|
1999
|
+
lower = self._parse_insane_lipid_args(lipid_parts[0], ratio_parts[0])
|
|
2000
|
+
upper = self._parse_insane_lipid_args(lipid_parts[1], ratio_parts[1])
|
|
2001
|
+
if lower is None or upper is None:
|
|
2002
|
+
return None
|
|
2003
|
+
else:
|
|
2004
|
+
if "//" in ratio_spec:
|
|
2005
|
+
return None
|
|
2006
|
+
lower = self._parse_insane_lipid_args(lipid_spec, ratio_spec)
|
|
2007
|
+
upper = None
|
|
2008
|
+
if lower is None:
|
|
2009
|
+
return None
|
|
2010
|
+
if index == 0:
|
|
2011
|
+
lower_flag = "-l"
|
|
2012
|
+
upper_flag = "-u"
|
|
2013
|
+
else:
|
|
2014
|
+
lower_flag = "-lo"
|
|
2015
|
+
upper_flag = "-uo"
|
|
2016
|
+
for name, ratio in lower:
|
|
2017
|
+
args.extend([lower_flag, f"{name}:{ratio}"])
|
|
2018
|
+
if upper is not None:
|
|
2019
|
+
for name, ratio in upper:
|
|
2020
|
+
args.extend([upper_flag, f"{name}:{ratio}"])
|
|
2021
|
+
if self.dims and self.dims != [0, 0, 0]:
|
|
2022
|
+
dims_nm = [value / 10.0 for value in self.dims]
|
|
2023
|
+
args.extend(["-x", str(dims_nm[0]), "-y", str(dims_nm[1]), "-z", str(dims_nm[2])])
|
|
2024
|
+
if self.curvature is not None and abs(self.curvature) > 0:
|
|
2025
|
+
curv_abs = abs(self.curvature)
|
|
2026
|
+
curv_dir = 1 if self.curvature >= 0 else -1
|
|
2027
|
+
args.extend(["-curv", f"{curv_abs},0,{curv_dir}"])
|
|
2028
|
+
if self.solvents:
|
|
2029
|
+
solvent_names = [s for s in self.solvents.split(":") if s]
|
|
2030
|
+
solvent_ratios = [r for r in (self.solvent_ratio or "").split(":") if r]
|
|
2031
|
+
if solvent_ratios and len(solvent_names) != len(solvent_ratios):
|
|
2032
|
+
return None
|
|
2033
|
+
if not solvent_ratios:
|
|
2034
|
+
solvent_ratios = ["1"] * len(solvent_names)
|
|
2035
|
+
warned_wat = False
|
|
2036
|
+
for name, ratio in zip(solvent_names, solvent_ratios):
|
|
2037
|
+
sol_name = name
|
|
2038
|
+
if name.upper() == "WAT":
|
|
2039
|
+
if not warned_wat:
|
|
2040
|
+
logger.warning(
|
|
2041
|
+
"WARNING:\n Martini solvent WAT mapped to Insane solvent W."
|
|
2042
|
+
)
|
|
2043
|
+
warned_wat = True
|
|
2044
|
+
sol_name = "W"
|
|
2045
|
+
args.extend(["-sol", f"{sol_name}:{ratio}"])
|
|
2046
|
+
if self.salt:
|
|
2047
|
+
def _ion_token(name):
|
|
2048
|
+
return re.sub(r"[^A-Za-z]", "", name).upper()
|
|
2049
|
+
neg = _ion_token(self.salt_a)
|
|
2050
|
+
pos = _ion_token(self.salt_c)
|
|
2051
|
+
if neg:
|
|
2052
|
+
args.extend(["-negi_c0", neg, "-negi_c1", neg, "-negi_c2", neg])
|
|
2053
|
+
if pos:
|
|
2054
|
+
args.extend(["-posi_c0", pos, "-posi_c1", pos, "-posi_c2", pos])
|
|
2055
|
+
conc = f"{self.saltcon},{self.saltcon},{self.saltcon}"
|
|
2056
|
+
args.extend(["-ion_conc", conc])
|
|
2057
|
+
else:
|
|
2058
|
+
args.extend([
|
|
2059
|
+
"-negi_c0", "CL", "-negi_c1", "CL", "-negi_c2", "CL",
|
|
2060
|
+
"-posi_c0", "NA", "-posi_c1", "NA", "-posi_c2", "NA",
|
|
2061
|
+
])
|
|
2062
|
+
return " ".join(args) if args else None
|
|
2063
|
+
|
|
2064
|
+
def _parse_insane_lipid_args(self, lipid_spec, ratio_spec):
|
|
2065
|
+
lipid_names = [l for l in lipid_spec.split(":") if l]
|
|
2066
|
+
ratio_vals = [r for r in ratio_spec.split(":") if r] if ratio_spec else []
|
|
2067
|
+
if ratio_vals and len(lipid_names) != len(ratio_vals):
|
|
2068
|
+
return None
|
|
2069
|
+
if not ratio_vals:
|
|
2070
|
+
ratio_vals = ["1"] * len(lipid_names)
|
|
2071
|
+
return list(zip(lipid_names, ratio_vals))
|
|
2072
|
+
|
|
2073
|
+
def _warn_martini_unsupported(self):
|
|
2074
|
+
def warn(flag, message):
|
|
2075
|
+
if flag:
|
|
2076
|
+
logger.warning("WARNING:\n %s", message)
|
|
2077
|
+
warn(self.distxy_fix is not None, "--distxy_fix is not supported for Martini Insane builds; ignoring.")
|
|
2078
|
+
warn("--dist" in sys.argv, "--dist is not supported for Martini Insane builds; ignoring.")
|
|
2079
|
+
warn("--dist_wat" in sys.argv, "--dist_wat is not supported for Martini Insane builds; ignoring.")
|
|
2080
|
+
warn(self.xygauss is not None, "--xygauss is not supported for Martini Insane builds; ignoring.")
|
|
2081
|
+
warn(self.apl_offset is not None, "--apl_offset is not supported for Martini Insane builds; ignoring.")
|
|
2082
|
+
warn("--lip_offset" in sys.argv, "--lip_offset is not supported for Martini Insane builds; ignoring.")
|
|
2083
|
+
warn(self.pbc, "--pbc is not supported for Martini Insane builds; ignoring.")
|
|
2084
|
+
warn(self.nocounter, "--nocounter is not supported for Martini Insane builds; ignoring.")
|
|
2085
|
+
warn(self.charge_imbalance != 0, "--charge_imbalance is not supported for Martini Insane builds; ignoring.")
|
|
2086
|
+
warn("--imbalance_ion" in sys.argv, "--imbalance_ion is not supported for Martini Insane builds; ignoring.")
|
|
2087
|
+
|
|
2088
|
+
def _postprocess_martini_topology(self, cg_dir, top_path, itp_paths, source_top=None):
|
|
2089
|
+
if not os.path.exists(top_path):
|
|
2090
|
+
logger.warning("WARNING:\n Topology file not found at %s; skipping postprocess.", top_path)
|
|
2091
|
+
return
|
|
2092
|
+
cg_top_path = None
|
|
2093
|
+
if source_top and os.path.exists(source_top):
|
|
2094
|
+
cg_top_path = source_top
|
|
2095
|
+
else:
|
|
2096
|
+
cg_top_candidates = sorted(glob.glob(os.path.join(cg_dir, "*-cg.top"))) + \
|
|
2097
|
+
sorted(glob.glob(os.path.join(cg_dir, "*_cg.top")))
|
|
2098
|
+
if cg_top_candidates:
|
|
2099
|
+
cg_top_path = cg_top_candidates[0]
|
|
2100
|
+
if not cg_top_path:
|
|
2101
|
+
logger.warning(
|
|
2102
|
+
"WARNING:\n No martinize/topology source found (expected *_topol.top or *-cg.top). Skipping molecules replacement."
|
|
2103
|
+
)
|
|
2104
|
+
return
|
|
2105
|
+
with open(cg_top_path, "r") as handle:
|
|
2106
|
+
cg_lines = handle.readlines()
|
|
2107
|
+
cg_mol_lines = self._extract_molecules_section(cg_lines)
|
|
2108
|
+
if not cg_mol_lines:
|
|
2109
|
+
logger.warning("WARNING:\n [molecules] section not found in %s; skipping molecules replacement.", cg_top_path)
|
|
2110
|
+
return
|
|
2111
|
+
with open(top_path, "r") as handle:
|
|
2112
|
+
top_lines = handle.readlines()
|
|
2113
|
+
top_lines = self._replace_protein_in_molecules(top_lines, cg_mol_lines)
|
|
2114
|
+
if itp_paths:
|
|
2115
|
+
itp_names = [os.path.basename(p) for p in sorted(itp_paths)]
|
|
2116
|
+
top_lines = self._replace_protein_include(top_lines, itp_names)
|
|
2117
|
+
with open(top_path, "w") as handle:
|
|
2118
|
+
handle.writelines(top_lines)
|
|
2119
|
+
|
|
2120
|
+
def _extract_molecules_section(self, lines):
|
|
2121
|
+
in_section = False
|
|
2122
|
+
collected = []
|
|
2123
|
+
for line in lines:
|
|
2124
|
+
stripped = line.strip().lower()
|
|
2125
|
+
if stripped.startswith("[") and stripped.endswith("]"):
|
|
2126
|
+
if in_section:
|
|
2127
|
+
break
|
|
2128
|
+
if "molecules" in stripped:
|
|
2129
|
+
in_section = True
|
|
2130
|
+
continue
|
|
2131
|
+
if in_section:
|
|
2132
|
+
collected.append(line)
|
|
2133
|
+
return collected
|
|
2134
|
+
|
|
2135
|
+
def _replace_protein_in_molecules(self, lines, replacement_lines):
|
|
2136
|
+
in_section = False
|
|
2137
|
+
replaced = False
|
|
2138
|
+
output = []
|
|
2139
|
+
for line in lines:
|
|
2140
|
+
stripped = line.strip().lower()
|
|
2141
|
+
if stripped.startswith("[") and stripped.endswith("]"):
|
|
2142
|
+
in_section = "molecules" in stripped
|
|
2143
|
+
output.append(line)
|
|
2144
|
+
continue
|
|
2145
|
+
if in_section and not replaced and stripped and not stripped.startswith(";"):
|
|
2146
|
+
parts = stripped.split()
|
|
2147
|
+
if parts and parts[0] == "protein":
|
|
2148
|
+
output.extend(replacement_lines)
|
|
2149
|
+
replaced = True
|
|
2150
|
+
continue
|
|
2151
|
+
output.append(line)
|
|
2152
|
+
return output
|
|
2153
|
+
|
|
2154
|
+
def _replace_protein_include(self, lines, itp_names):
|
|
2155
|
+
output = []
|
|
2156
|
+
replaced = False
|
|
2157
|
+
for line in lines:
|
|
2158
|
+
if not replaced and "protein-cg" in line.lower() and line.strip().startswith("#include"):
|
|
2159
|
+
for name in itp_names:
|
|
2160
|
+
output.append(f'#include "{name}"\n')
|
|
2161
|
+
replaced = True
|
|
2162
|
+
continue
|
|
2163
|
+
output.append(line)
|
|
2164
|
+
return output
|
|
2165
|
+
|
|
2166
|
+
def _local_output_path(self, pdb, suffix):
|
|
2167
|
+
base = os.path.basename(pdb)
|
|
2168
|
+
stem = base[:-4] if base.endswith(".pdb") else base
|
|
2169
|
+
return self._out_path(stem + suffix)
|
|
2170
|
+
|
|
2171
|
+
def _insane_data_path(self, filename):
|
|
2172
|
+
return os.path.join(script_path, "data", filename)
|
|
2173
|
+
|
|
2174
|
+
def _read_insane_list_file(self, path):
|
|
2175
|
+
if not os.path.exists(path):
|
|
2176
|
+
return set()
|
|
2177
|
+
items = set()
|
|
2178
|
+
try:
|
|
2179
|
+
with open(path, "r") as handle:
|
|
2180
|
+
for line in handle:
|
|
2181
|
+
line = line.strip()
|
|
2182
|
+
if not line or line.startswith("#"):
|
|
2183
|
+
continue
|
|
2184
|
+
items.add(line)
|
|
2185
|
+
except:
|
|
2186
|
+
return set()
|
|
2187
|
+
return items
|
|
2188
|
+
|
|
2189
|
+
def _write_insane_list_file(self, path, items, source_path):
|
|
2190
|
+
try:
|
|
2191
|
+
with open(path, "w") as handle:
|
|
2192
|
+
handle.write("# Generated from Insane4MemPrO\n")
|
|
2193
|
+
handle.write(f"# Source: {source_path}\n")
|
|
2194
|
+
handle.write(f"# Count: {len(items)}\n")
|
|
2195
|
+
for name in sorted(items):
|
|
2196
|
+
handle.write(name + "\n")
|
|
2197
|
+
except:
|
|
2198
|
+
pass
|
|
2199
|
+
|
|
2200
|
+
def _locate_insane_file(self):
|
|
2201
|
+
env_path = os.environ.get("PATH_TO_INSANE", "").strip()
|
|
2202
|
+
if env_path and os.path.exists(env_path):
|
|
2203
|
+
return env_path
|
|
2204
|
+
for mod_name in ("mempro", "MemPrO"):
|
|
2205
|
+
spec = importlib.util.find_spec(mod_name)
|
|
2206
|
+
if spec and spec.submodule_search_locations:
|
|
2207
|
+
base = spec.submodule_search_locations[0]
|
|
2208
|
+
candidate = os.path.join(base, "Insane4MemPrO.py")
|
|
2209
|
+
if os.path.exists(candidate):
|
|
2210
|
+
return candidate
|
|
2211
|
+
return None
|
|
2212
|
+
|
|
2213
|
+
def _load_insane_lipids(self):
|
|
2214
|
+
data_path = self._insane_data_path("insane_lipids.txt")
|
|
2215
|
+
cached = self._read_insane_list_file(data_path)
|
|
2216
|
+
if cached:
|
|
2217
|
+
return cached
|
|
2218
|
+
insane_path = self._locate_insane_file()
|
|
2219
|
+
if not insane_path or not os.path.exists(insane_path):
|
|
2220
|
+
return set()
|
|
2221
|
+
try:
|
|
2222
|
+
with open(insane_path, "r") as handle:
|
|
2223
|
+
content = handle.read()
|
|
2224
|
+
except:
|
|
2225
|
+
return set()
|
|
2226
|
+
keys = set()
|
|
2227
|
+
lines = content.splitlines()
|
|
2228
|
+
in_block = False
|
|
2229
|
+
depth = 0
|
|
2230
|
+
for line in lines:
|
|
2231
|
+
if not in_block:
|
|
2232
|
+
if "lipidsa.update" in line and "{" in line:
|
|
2233
|
+
in_block = True
|
|
2234
|
+
depth = line.count("{") - line.count("}")
|
|
2235
|
+
continue
|
|
2236
|
+
depth += line.count("{") - line.count("}")
|
|
2237
|
+
for match in re.findall(r"[\"']([^\"']+)[\"']\\s*:", line):
|
|
2238
|
+
keys.add(match)
|
|
2239
|
+
if depth <= 0:
|
|
2240
|
+
in_block = False
|
|
2241
|
+
keys.update(set(re.findall(r"lipidsa\\[\\s*[\"']([^\"']+)[\"']\\s*\\]", content)))
|
|
2242
|
+
if keys:
|
|
2243
|
+
self._write_insane_list_file(data_path, keys, insane_path)
|
|
2244
|
+
return keys
|
|
2245
|
+
|
|
2246
|
+
def _load_insane_solvents(self):
|
|
2247
|
+
data_path = self._insane_data_path("insane_solvents.txt")
|
|
2248
|
+
cached = self._read_insane_list_file(data_path)
|
|
2249
|
+
if cached:
|
|
2250
|
+
return cached
|
|
2251
|
+
insane_path = self._locate_insane_file()
|
|
2252
|
+
if not insane_path or not os.path.exists(insane_path):
|
|
2253
|
+
return set()
|
|
2254
|
+
try:
|
|
2255
|
+
with open(insane_path, "r") as handle:
|
|
2256
|
+
content = handle.read()
|
|
2257
|
+
except:
|
|
2258
|
+
return set()
|
|
2259
|
+
keys = set()
|
|
2260
|
+
lines = content.splitlines()
|
|
2261
|
+
in_block = False
|
|
2262
|
+
depth = 0
|
|
2263
|
+
for line in lines:
|
|
2264
|
+
if not in_block:
|
|
2265
|
+
if "solventParticles" in line and "{" in line:
|
|
2266
|
+
in_block = True
|
|
2267
|
+
depth = line.count("{") - line.count("}")
|
|
2268
|
+
continue
|
|
2269
|
+
depth += line.count("{") - line.count("}")
|
|
2270
|
+
match = re.search(r"[\"']([^\"']+)[\"']\\s*:", line)
|
|
2271
|
+
if match:
|
|
2272
|
+
keys.add(match.group(1))
|
|
2273
|
+
if depth <= 0:
|
|
2274
|
+
in_block = False
|
|
2275
|
+
break
|
|
2276
|
+
for line in lines:
|
|
2277
|
+
if "for s in [" in line:
|
|
2278
|
+
list_content = line.split("[", 1)[1].rsplit("]", 1)[0]
|
|
2279
|
+
for item in list_content.split(","):
|
|
2280
|
+
item = item.strip().strip('"').strip("'")
|
|
2281
|
+
if item:
|
|
2282
|
+
keys.add(item)
|
|
2283
|
+
break
|
|
2284
|
+
if keys:
|
|
2285
|
+
self._write_insane_list_file(data_path, keys, insane_path)
|
|
2286
|
+
return keys
|
|
2287
|
+
|
|
2288
|
+
def _martinize2(self, pdb, overwrite=False):
|
|
2289
|
+
base = os.path.basename(pdb)
|
|
2290
|
+
stem = base[:-4] if base.endswith(".pdb") else base
|
|
2291
|
+
output = self._local_output_path(pdb, "_martinized.pdb")
|
|
2292
|
+
topol = self._out_path(f"{stem}_topol.top")
|
|
2293
|
+
if os.path.exists(output) and not overwrite:
|
|
2294
|
+
logger.info("Martinize2 output exists at %s; skipping martinize2 execution.", output)
|
|
2295
|
+
return output
|
|
2296
|
+
martinize_cmd = shutil.which("martinize2")
|
|
2297
|
+
if not martinize_cmd:
|
|
2298
|
+
logger.critical("CRITICAL:\n martinize2 not found. Install it or provide a pre-martinized input with --martinized.")
|
|
2299
|
+
exit()
|
|
2300
|
+
cmd = [
|
|
2301
|
+
martinize_cmd,
|
|
2302
|
+
"-f", pdb,
|
|
2303
|
+
"-x", output,
|
|
2304
|
+
"-o", topol,
|
|
2305
|
+
"-ff", "martini3001",
|
|
2306
|
+
"-dssp",
|
|
2307
|
+
"-elastic",
|
|
2308
|
+
"-scfix",
|
|
2309
|
+
"-eu", "0.9",
|
|
2310
|
+
"-el", "0.5",
|
|
2311
|
+
"-ea", "0",
|
|
2312
|
+
"-ep", "0",
|
|
2313
|
+
"-merge", "A",
|
|
2314
|
+
"-resid", "input",
|
|
2315
|
+
"-maxwarn", "99999"
|
|
2316
|
+
]
|
|
2317
|
+
logger.info("Running martinize2: %s", " ".join(cmd))
|
|
2318
|
+
result = subprocess.run(cmd)
|
|
2319
|
+
if result.returncode != 0:
|
|
2320
|
+
logger.critical("CRITICAL:\n martinize2 failed. Check its output and inputs.")
|
|
2321
|
+
exit()
|
|
2322
|
+
self._used_tools.add("martini")
|
|
2323
|
+
return output
|
|
2324
|
+
|
|
2325
|
+
def mempro_align(self,pdb,keepligs=False,double_span=False,verbose=False,overwrite=False,n_ter="_in"):
|
|
2326
|
+
output = self._local_output_path(pdb, n_ter + "_MEMPRO.pdb")
|
|
2327
|
+
lig_output = output.replace("_MEMPRO.pdb", "_MEMPRO_ligs.pdb")
|
|
2328
|
+
if lig_output == output:
|
|
2329
|
+
lig_output = output[:-4] + "_ligs.pdb" if output.lower().endswith(".pdb") else output + "_ligs.pdb"
|
|
2330
|
+
pdb_base = os.path.basename(pdb)
|
|
2331
|
+
tmp_prefix = "" if self.keep_mempro else "_tmp_"
|
|
2332
|
+
tmp_folder = self._out_path(tmp_prefix + pdb_base[:-4] + n_ter + "_MEMPRO")
|
|
2333
|
+
out_dir = tmp_folder + os.path.sep
|
|
2334
|
+
info_path = os.path.join(tmp_folder, "Rank_1", "info_rank_1.txt")
|
|
2335
|
+
cg_dir = os.path.join(tmp_folder, "Rank_1", "CG_System_rank_1")
|
|
2336
|
+
if os.path.exists(output) and not overwrite:
|
|
2337
|
+
logger.info("MemPrO output exists at %s; skipping MemPrO execution.", output)
|
|
2338
|
+
self._apply_mempro_curvature(info_path)
|
|
2339
|
+
if self.martini and self.build_system is not None:
|
|
2340
|
+
self.martini_build_output = cg_dir if os.path.exists(cg_dir) else None
|
|
2341
|
+
self._finalize_martini_build()
|
|
2342
|
+
if keepligs:
|
|
2343
|
+
if verbose:
|
|
2344
|
+
logger.info("Superimposing to keep ligands")
|
|
2345
|
+
if overwrite or not os.path.exists(lig_output):
|
|
2346
|
+
rmsd_of_pdbs(pdb, output, transform_pdb1=lig_output, standard=True)
|
|
2347
|
+
output = lig_output
|
|
2348
|
+
if double_span:
|
|
2349
|
+
if not os.path.exists(info_path):
|
|
2350
|
+
logger.warning(
|
|
2351
|
+
"MemPrO info_rank_1.txt not found for cached output; "
|
|
2352
|
+
"using z_offset=0.0. Rerun with --overwrite to regenerate."
|
|
2353
|
+
)
|
|
2354
|
+
return (output, 0.0)
|
|
2355
|
+
z_offset = None
|
|
2356
|
+
with open(info_path, "r") as f:
|
|
2357
|
+
for line in f:
|
|
2358
|
+
if line.startswith("Iter-Membrane distance"):
|
|
2359
|
+
try:
|
|
2360
|
+
z_offset = float(line.split(":")[1].split()[0])
|
|
2361
|
+
except:
|
|
2362
|
+
z_offset = None
|
|
2363
|
+
break
|
|
2364
|
+
if z_offset is None:
|
|
2365
|
+
logger.warning(
|
|
2366
|
+
"Could not parse inter-membrane distance from cached MemPrO output; "
|
|
2367
|
+
"using z_offset=0.0. Rerun with --overwrite to regenerate."
|
|
2368
|
+
)
|
|
2369
|
+
z_offset = 0.0
|
|
2370
|
+
return (output, z_offset)
|
|
2371
|
+
return output
|
|
2372
|
+
if not os.path.exists(output) or overwrite:
|
|
2373
|
+
if not os.path.exists(tmp_folder):
|
|
2374
|
+
os.mkdir(tmp_folder)
|
|
2375
|
+
if self.keep_mempro:
|
|
2376
|
+
self.created_mempro.append(tmp_folder)
|
|
2377
|
+
else:
|
|
2378
|
+
self.created.append(tmp_folder)
|
|
2379
|
+
if "NUM_CPU" not in os.environ or not os.environ["NUM_CPU"].strip():
|
|
2380
|
+
os.environ["NUM_CPU"] = str(os.cpu_count() or 1)
|
|
2381
|
+
if "PATH_TO_MARTINI" not in os.environ or not os.environ["PATH_TO_MARTINI"].strip():
|
|
2382
|
+
martini_candidates = sorted(glob.glob(os.path.join(script_path, "data", "*martini*.itp")))
|
|
2383
|
+
if martini_candidates:
|
|
2384
|
+
os.environ["PATH_TO_MARTINI"] = martini_candidates[0]
|
|
2385
|
+
else:
|
|
2386
|
+
logger.critical("CRITICAL:\n PATH_TO_MARTINI is required for MemPrO. Place a martini itp in data/ or set the env var.")
|
|
2387
|
+
exit()
|
|
2388
|
+
if "PATH_TO_INSANE" not in os.environ or not os.environ["PATH_TO_INSANE"].strip():
|
|
2389
|
+
insane_path = None
|
|
2390
|
+
for mod_name in ("mempro", "MemPrO"):
|
|
2391
|
+
spec = importlib.util.find_spec(mod_name)
|
|
2392
|
+
if spec and spec.submodule_search_locations:
|
|
2393
|
+
base = spec.submodule_search_locations[0]
|
|
2394
|
+
candidate = os.path.join(base, "Insane4MemPrO.py")
|
|
2395
|
+
if os.path.exists(candidate):
|
|
2396
|
+
insane_path = candidate
|
|
2397
|
+
break
|
|
2398
|
+
if insane_path:
|
|
2399
|
+
os.environ["PATH_TO_INSANE"] = insane_path
|
|
2400
|
+
if self.build_system is not None and not self.martini:
|
|
2401
|
+
logger.critical("CRITICAL:\n --insane_build_ranks requires --martini.")
|
|
2402
|
+
exit()
|
|
2403
|
+
if self.build_arguments and not self.martini:
|
|
2404
|
+
logger.critical("CRITICAL:\n --insane_args requires --martini.")
|
|
2405
|
+
exit()
|
|
2406
|
+
if self.martini and self.build_system is None:
|
|
2407
|
+
self.build_system = 1
|
|
2408
|
+
if self.martini and self.build_system is not None and not (self.build_arguments and self.build_arguments.strip()):
|
|
2409
|
+
auto_args = self._auto_build_insane_args()
|
|
2410
|
+
if auto_args:
|
|
2411
|
+
logger.info("Auto-constructed --insane_args: %s", auto_args)
|
|
2412
|
+
self.build_arguments = auto_args
|
|
2413
|
+
else:
|
|
2414
|
+
logger.critical("CRITICAL:\n -bd_args must be supplied when using -bd.")
|
|
2415
|
+
exit()
|
|
2416
|
+
if self.martini and ("PATH_TO_INSANE" not in os.environ or not os.environ["PATH_TO_INSANE"].strip()):
|
|
2417
|
+
logger.critical("CRITICAL:\n PATH_TO_INSANE is required for MemPrO --martini. Set the env var or install Insane4MemPrO.")
|
|
2418
|
+
exit()
|
|
2419
|
+
if not self.mempro:
|
|
2420
|
+
logger.critical("CRITICAL:\n MemPrO executable not found. Use --mempro to point to MemPrO.")
|
|
2421
|
+
exit()
|
|
2422
|
+
if keepligs:
|
|
2423
|
+
logger.warning("WARNING: MemPrO ignores HETATM records; ligands may be dropped during orientation.")
|
|
2424
|
+
if os.path.isfile(self.mempro) and self.mempro.endswith(".py"):
|
|
2425
|
+
cmd = [sys.executable, self.mempro]
|
|
2426
|
+
else:
|
|
2427
|
+
cmd = [self.mempro]
|
|
2428
|
+
cmd += ["-f", pdb, "-o", out_dir, "-ng", str(self.mempro_grid), "-ni", str(self.mempro_iters), "-rank", self.mempro_rank]
|
|
2429
|
+
mempro_arg_str = self.mempro_args or ""
|
|
2430
|
+
if "-mt" not in mempro_arg_str and "--membrane_thickness" not in mempro_arg_str:
|
|
2431
|
+
cmd += ["-mt", str(self.leaflet)]
|
|
2432
|
+
if self.martini and self.build_system is not None:
|
|
2433
|
+
cmd += ["-bd", str(self.build_system)]
|
|
2434
|
+
if self.build_arguments:
|
|
2435
|
+
cmd += ["-bd_args", self.build_arguments]
|
|
2436
|
+
if double_span:
|
|
2437
|
+
cmd.append("-dm")
|
|
2438
|
+
if self.mempro_curvature:
|
|
2439
|
+
cmd.append("-c")
|
|
2440
|
+
if self.mempro_args:
|
|
2441
|
+
cmd += shlex.split(self.mempro_args)
|
|
2442
|
+
if verbose:
|
|
2443
|
+
logger.info("Running MemPrO: %s", " ".join(cmd))
|
|
2444
|
+
result = subprocess.run(cmd)
|
|
2445
|
+
if result.returncode != 0:
|
|
2446
|
+
logger.critical("CRITICAL:\n MemPrO failed to orient the protein. Check MemPrO logs and inputs.")
|
|
2447
|
+
exit()
|
|
2448
|
+
oriented = os.path.join(tmp_folder, "Rank_1", "oriented_rank_1.pdb")
|
|
2449
|
+
if not os.path.exists(oriented):
|
|
2450
|
+
logger.critical("CRITICAL:\n MemPrO output not found at %s", oriented)
|
|
2451
|
+
exit()
|
|
2452
|
+
shutil.copy(oriented, output)
|
|
2453
|
+
cleaned = []
|
|
2454
|
+
with open(output, "r") as f:
|
|
2455
|
+
for line in f:
|
|
2456
|
+
if not (line.startswith("ATOM") or line.startswith("HETATM")):
|
|
2457
|
+
continue
|
|
2458
|
+
if line[17:20].strip() == "DUM":
|
|
2459
|
+
continue
|
|
2460
|
+
cleaned.append(line)
|
|
2461
|
+
with open(output, "w") as f:
|
|
2462
|
+
f.writelines(cleaned)
|
|
2463
|
+
if keepligs:
|
|
2464
|
+
if verbose:
|
|
2465
|
+
logger.info("Superimposing to keep ligands")
|
|
2466
|
+
if overwrite or not os.path.exists(lig_output):
|
|
2467
|
+
rmsd_of_pdbs(pdb, output, transform_pdb1=lig_output, standard=True)
|
|
2468
|
+
output = lig_output
|
|
2469
|
+
if self.martini and self.build_system is not None:
|
|
2470
|
+
self.martini_build_output = cg_dir if os.path.exists(cg_dir) else None
|
|
2471
|
+
self._apply_mempro_curvature(info_path)
|
|
2472
|
+
if double_span:
|
|
2473
|
+
if not os.path.exists(info_path):
|
|
2474
|
+
logger.critical("CRITICAL:\n MemPrO info_rank_1.txt not found for double_span orientation.")
|
|
2475
|
+
exit()
|
|
2476
|
+
z_offset = None
|
|
2477
|
+
with open(info_path, "r") as f:
|
|
2478
|
+
for line in f:
|
|
2479
|
+
if line.startswith("Iter-Membrane distance"):
|
|
2480
|
+
try:
|
|
2481
|
+
z_offset = float(line.split(":")[1].split()[0])
|
|
2482
|
+
except:
|
|
2483
|
+
z_offset = None
|
|
2484
|
+
break
|
|
2485
|
+
if z_offset is None:
|
|
2486
|
+
logger.critical("CRITICAL:\n Could not parse inter-membrane distance from MemPrO output.")
|
|
2487
|
+
exit()
|
|
2488
|
+
return (output, z_offset)
|
|
2489
|
+
return output
|
|
2490
|
+
|
|
2491
|
+
def _parse_mempro_global_curvature(self, info_path):
|
|
2492
|
+
if not os.path.exists(info_path):
|
|
2493
|
+
return None
|
|
2494
|
+
with open(info_path, "r") as handle:
|
|
2495
|
+
for line in handle:
|
|
2496
|
+
if "Global curvature" not in line:
|
|
2497
|
+
continue
|
|
2498
|
+
try:
|
|
2499
|
+
return float(line.split(":")[1].split()[0])
|
|
2500
|
+
except:
|
|
2501
|
+
return None
|
|
2502
|
+
return None
|
|
2503
|
+
|
|
2504
|
+
def _apply_mempro_curvature(self, info_path):
|
|
2505
|
+
if not self.mempro_curvature:
|
|
2506
|
+
return
|
|
2507
|
+
curvature = self._parse_mempro_global_curvature(info_path)
|
|
2508
|
+
if curvature is None:
|
|
2509
|
+
logger.warning("MemPrO Global curvature not found in %s; --curvature unchanged.", info_path)
|
|
2510
|
+
return
|
|
2511
|
+
if abs(curvature) < 1e-9:
|
|
2512
|
+
logger.warning("MemPrO Global curvature is ~0.0; treating membrane as flat.")
|
|
2513
|
+
return
|
|
2514
|
+
if self.curvature is not None:
|
|
2515
|
+
logger.info("MemPrO Global curvature available; keeping user-provided --curvature=%s.", self.curvature)
|
|
2516
|
+
return
|
|
2517
|
+
if self.vol or self.solvate:
|
|
2518
|
+
logger.critical("CRITICAL:\n MemPrO curvature is not compatible with --vol or --solvate.")
|
|
2519
|
+
exit()
|
|
2520
|
+
self.curvature = curvature
|
|
2521
|
+
self.curv_radius = 1 / curvature
|
|
2522
|
+
logger.info("Using MemPrO Global curvature as --curvature: %s", curvature)
|
|
2523
|
+
|
|
2524
|
+
def pdbvol(self,pdb,spacing=0.5,overwrite=False):
|
|
2525
|
+
output = self._local_output_path(pdb, ".grid.pdb")
|
|
2526
|
+
if os.path.exists(output) and not overwrite:
|
|
2527
|
+
filelength = len(open(output,"r").readlines())
|
|
2528
|
+
vol = filelength*spacing**3
|
|
2529
|
+
return (output, vol)
|
|
2530
|
+
else:
|
|
2531
|
+
logger.debug("PDBREMIX grid start: pdb=%s spacing=%s output=%s", pdb, spacing, output)
|
|
2532
|
+
filelines = open(pdb,"r").readlines()
|
|
2533
|
+
temp_pdb = []
|
|
2534
|
+
for line in filelines:
|
|
2535
|
+
if (line[0:4] == "ATOM" or line[0:6] == "HETATM") and line[17:20].strip() != "DUM":
|
|
2536
|
+
temp_pdb.append(line)
|
|
2537
|
+
logger.debug("PDBREMIX grid: input lines=%s kept=%s", len(filelines), len(temp_pdb))
|
|
2538
|
+
tmp_path = self._out_path("temp.pdb")
|
|
2539
|
+
open(tmp_path,"w").writelines(temp_pdb)
|
|
2540
|
+
logger.debug("PDBREMIX grid: temp.pdb written, reading with pdbremix")
|
|
2541
|
+
atoms = pdbatoms.read_pdb(tmp_path)
|
|
2542
|
+
pdbatoms.add_radii(atoms)
|
|
2543
|
+
logger.debug("PDBREMIX grid: calling volume()")
|
|
2544
|
+
vol = volume(atoms, spacing, output, verbose=False)
|
|
2545
|
+
logger.debug("PDBREMIX grid: volume() done, vol=%s", vol)
|
|
2546
|
+
os.remove(tmp_path)
|
|
2547
|
+
return (output, vol)
|
|
2548
|
+
|
|
2549
|
+
def run_packmol(self):
|
|
2550
|
+
os.makedirs(self.outdir, exist_ok=True)
|
|
2551
|
+
os.makedirs(os.path.dirname(self.packlog) or ".", exist_ok=True)
|
|
2552
|
+
with open(self.packlog+".inp","w+") as script:
|
|
2553
|
+
script.write(self.contents)
|
|
2554
|
+
script.seek(0, os.SEEK_SET)
|
|
2555
|
+
logger.debug("Script for packmol written to "+self.packlog+".inp")
|
|
2556
|
+
|
|
2557
|
+
if self.packmol_cmd and self.run:
|
|
2558
|
+
self._used_tools.add("packmol")
|
|
2559
|
+
logger.info("\nRunning Packmol...")
|
|
2560
|
+
log = open(self.packlog+".log","w")
|
|
2561
|
+
p = subprocess.Popen(self.packmol_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=script)
|
|
2562
|
+
global pid
|
|
2563
|
+
pid = p.pid
|
|
2564
|
+
def kill_child():
|
|
2565
|
+
if pid is None:
|
|
2566
|
+
pass
|
|
2567
|
+
else:
|
|
2568
|
+
try:
|
|
2569
|
+
os.kill(pid, signal.SIGTERM)
|
|
2570
|
+
except:
|
|
2571
|
+
pass
|
|
2572
|
+
atexit.register(kill_child)
|
|
2573
|
+
GENCAN_track = 0
|
|
2574
|
+
TOTGENCAN = 0
|
|
2575
|
+
FRAME = 0
|
|
2576
|
+
energy_values = []
|
|
2577
|
+
starting = True
|
|
2578
|
+
all_together = False
|
|
2579
|
+
progress_bar = True
|
|
2580
|
+
step = 1
|
|
2581
|
+
while True:
|
|
2582
|
+
output = p.stdout.readline().decode('utf-8')
|
|
2583
|
+
log.write(output)
|
|
2584
|
+
if output == '' and p.poll() is not None:
|
|
2585
|
+
break
|
|
2586
|
+
if "Number of independent structures:" in output.strip():
|
|
2587
|
+
structs = output.strip().split()[-1]
|
|
2588
|
+
if not self.packall:
|
|
2589
|
+
if not self.onlymembrane:
|
|
2590
|
+
TOTAL = int(self.nloop)*(int(structs)-1)+self.nloop_all
|
|
2591
|
+
else:
|
|
2592
|
+
TOTAL = int(self.nloop)*(int(structs))+self.nloop_all
|
|
2593
|
+
else:
|
|
2594
|
+
TOTAL = int(self.nloop_all)
|
|
2595
|
+
mag = int(math.log10(TOTAL))+2
|
|
2596
|
+
if not self.noprogress:
|
|
2597
|
+
p_bar = tqdm.tqdm(total=TOTAL)
|
|
2598
|
+
|
|
2599
|
+
if "Starting GENCAN loop" in output.strip():
|
|
2600
|
+
if GENCAN_track < self.nloop+1:
|
|
2601
|
+
GENCAN_track += 1; TOTGENCAN += 1
|
|
2602
|
+
if self.noprogress:
|
|
2603
|
+
if GENCAN_track % 10 == 0:
|
|
2604
|
+
sys.stdout.write(".")
|
|
2605
|
+
sys.stdout.flush()
|
|
2606
|
+
if not self.noprogress and GENCAN_track <= self.nloop:
|
|
2607
|
+
p_bar.update(1)
|
|
2608
|
+
if "Packing molecules " in output.strip():
|
|
2609
|
+
if self.noprogress:
|
|
2610
|
+
# sys.stdout.write("\033[F")
|
|
2611
|
+
sys.stdout.write("\r" +"\nProcessing segment "+output.strip().split()[-1]+" of "+structs)
|
|
2612
|
+
sys.stdout.flush()
|
|
2613
|
+
else:
|
|
2614
|
+
if int(self.nloop)-GENCAN_track > 0 and not starting:
|
|
2615
|
+
p_bar.update(int(self.nloop)-GENCAN_track)
|
|
2616
|
+
else:
|
|
2617
|
+
starting = False
|
|
2618
|
+
p_bar.set_description("Molecule segment %s/%s" % (output.strip().split()[-1],structs))
|
|
2619
|
+
GENCAN_track = 0
|
|
2620
|
+
if ("Current solution written to file" in output.strip() or "Writing current (perhaps bad) structure to file" in output.strip()) and self.traj:
|
|
2621
|
+
FRAME += 1
|
|
2622
|
+
shutil.move(self.outfile,self.outfile.replace(".pdb","_"+("{:0"+str(mag)+"d}").format(FRAME)+".pdb"))
|
|
2623
|
+
if "Packing all molecules together" in output.strip():
|
|
2624
|
+
all_together = True
|
|
2625
|
+
if not self.noprogress:
|
|
2626
|
+
if int(self.nloop)-GENCAN_track > 0 and not self.packall:
|
|
2627
|
+
p_bar.update(int(self.nloop)-GENCAN_track)
|
|
2628
|
+
p_bar.set_description("All-together Packing")
|
|
2629
|
+
logger.debug("\nIndividual packing processes complete. Initiating all-together packing. This might take a while!")
|
|
2630
|
+
sys.stdout.write("\033[F")
|
|
2631
|
+
sys.stdout.write("\r" +"\nAll-together Packing")
|
|
2632
|
+
sys.stdout.flush()
|
|
2633
|
+
GENCAN_track = 0
|
|
2634
|
+
self.nloop = self.nloop_all
|
|
2635
|
+
if "All-type function" in output.strip():
|
|
2636
|
+
fnx_value = float(output.strip().split()[-1])
|
|
2637
|
+
energy_values.append((TOTGENCAN,fnx_value))
|
|
2638
|
+
if "Function value from last GENCAN" in output.strip() and all_together:
|
|
2639
|
+
fnx_value = float(output.strip().split()[-1])
|
|
2640
|
+
energy_values.append((TOTGENCAN,fnx_value))
|
|
2641
|
+
if "Success!" in output.strip():
|
|
2642
|
+
if not self.noprogress:
|
|
2643
|
+
p_bar.update(int(self.nloop)-GENCAN_track)
|
|
2644
|
+
|
|
2645
|
+
if self.run and self.plot:
|
|
2646
|
+
try:
|
|
2647
|
+
import matplotlib
|
|
2648
|
+
matplotlib.use('Agg')
|
|
2649
|
+
import matplotlib.pyplot as plt
|
|
2650
|
+
matplotlib.rcParams["font.sans-serif"]='Arial'
|
|
2651
|
+
plt.plot(*list(zip(*energy_values)),color="black")
|
|
2652
|
+
plt.xlabel('Iteration')
|
|
2653
|
+
plt.ylabel('Objective function')
|
|
2654
|
+
plt.savefig(self.outfile+'.png')
|
|
2655
|
+
except:
|
|
2656
|
+
logger.error("ERROR:\n Matplotlib could not be imported. Check that you have a working version.")
|
|
2657
|
+
with open(self._out_path("GENCAN.log"),"w") as gencanlog:
|
|
2658
|
+
gencanlog.write("\n".join("%s %s" % x for x in energy_values))
|
|
2659
|
+
|
|
2660
|
+
if not os.path.exists(self.outfile):
|
|
2661
|
+
logger.critical("CRITICAL:\n No output file generated by PACKMOL. Check packmol.log and if the initial set of constraints are adequate for your system (e.g. leaflet size, tail and head planes)!")
|
|
2662
|
+
exit()
|
|
2663
|
+
|
|
2664
|
+
if self.sirah:
|
|
2665
|
+
fixwt4 = []
|
|
2666
|
+
with open(self.outfile, "r") as f:
|
|
2667
|
+
for line in f:
|
|
2668
|
+
if "COM WT4" in line:
|
|
2669
|
+
continue
|
|
2670
|
+
fixwt4.append(line)
|
|
2671
|
+
with open(self.outfile, "w") as f:
|
|
2672
|
+
f.writelines(fixwt4)
|
|
2673
|
+
else:
|
|
2674
|
+
logger.info("The script generated can be run by using a packmol executable (e.g. 'packmol < memgen.inp').")
|
|
2675
|
+
if self.sirah:
|
|
2676
|
+
logger.info("An artificial 'COM WT4' atom was added to avoid clashes. Make sure to delete it post packing and before parametrizing with 'grep -v \"COM WT4\"'")
|
|
2677
|
+
exit()
|
|
2678
|
+
|
|
2679
|
+
def postprocess(self):
|
|
2680
|
+
if not self.charmm:
|
|
2681
|
+
logger.info("Transforming to AMBER")
|
|
2682
|
+
charmmlipid2amber(self.outfile,self.outfile, os.path.join(script_path, "lib", "charmmlipid2amber", "charmmlipid2amber.csv"))
|
|
2683
|
+
|
|
2684
|
+
if self.sterols_PI_used:
|
|
2685
|
+
logger.info("Sterols and/or PI residues were packed. Potential piercing lipid tails will be searched")
|
|
2686
|
+
try:
|
|
2687
|
+
to_remove = find_piercing_lipids(self.outfile, verbose=True, hexadecimal_indices=self.hexadecimal_indices)
|
|
2688
|
+
try:
|
|
2689
|
+
if len(to_remove) > 0:
|
|
2690
|
+
self.outfile = remove_piercing_lipids(self.outfile, to_remove, outfile=self.outfile.replace(".pdb","_noclash.pdb"), verbose=True, hexadecimal_indices=self.hexadecimal_indices)
|
|
2691
|
+
except:
|
|
2692
|
+
logger.warning("Lipid piercing removal failed. Check your structure manually in case of clashing lipid tails!")
|
|
2693
|
+
except:
|
|
2694
|
+
logger.warning("Lipid piercing finder failed. Check your structure manually in case of clashing lipid tails!")
|
|
2695
|
+
|
|
2696
|
+
if self.hexadecimal_indices:
|
|
2697
|
+
convert_pdb_indices_to_hybrid36(self.outfile, atom_base=16, res_base=16)
|
|
2698
|
+
if self.xponge:
|
|
2699
|
+
logger.info("Applying Xponge ion names for compatibility with Xponge")
|
|
2700
|
+
apply_xponge_ion_names(self.outfile)
|
|
2701
|
+
|
|
2702
|
+
def cleanup(self):
|
|
2703
|
+
if self.delete:
|
|
2704
|
+
logger.debug("Deleting temporary files...")
|
|
2705
|
+
for file in self.created:
|
|
2706
|
+
try:
|
|
2707
|
+
if os.path.isfile(file):
|
|
2708
|
+
os.remove(file)
|
|
2709
|
+
elif os.path.isdir(file):
|
|
2710
|
+
shutil.rmtree(file)
|
|
2711
|
+
except:
|
|
2712
|
+
pass
|
|
2713
|
+
if self.run:
|
|
2714
|
+
for file in self.created_notrun:
|
|
2715
|
+
try:
|
|
2716
|
+
os.remove(file)
|
|
2717
|
+
except:
|
|
2718
|
+
pass
|
|
2719
|
+
|
|
2720
|
+
warn = "#Packing process finished. Check your final structure, particularly for lipids inserted in proteins, protein tunnels or piercing rings!#"
|
|
2721
|
+
print("\n"+"#"*len(warn))
|
|
2722
|
+
logger.info(warn)
|
|
2723
|
+
print("#"*len(warn))
|
|
2724
|
+
self._emit_references()
|
|
2725
|
+
print("DONE!")
|
|
2726
|
+
|
|
2727
|
+
def _emit_references(self):
|
|
2728
|
+
references = {
|
|
2729
|
+
"packmol-memgen": [
|
|
2730
|
+
"Schott-Verdugo, S.; Gohlke, H. "
|
|
2731
|
+
"PACKMOL-Memgen: A Simple-To-Use, Generalized Workflow for Membrane-Protein–Lipid-Bilayer System Building. "
|
|
2732
|
+
"J. Chem. Inf. Model. 2019, 59 (6), 2522–2528. https://doi.org/10.1021/acs.jcim.9b00269."
|
|
2733
|
+
],
|
|
2734
|
+
"packmol": [
|
|
2735
|
+
"Martínez, L.; Andrade, R.; Birgin, E. G.; Martínez, J. M. "
|
|
2736
|
+
"PACKMOL: A Package for Building Initial Configurations for Molecular Dynamics Simulations. "
|
|
2737
|
+
"J. Comput. Chem. 2009, 30 (13), 2157–2164. https://doi.org/10.1002/jcc.21224."
|
|
2738
|
+
],
|
|
2739
|
+
"mempro": [
|
|
2740
|
+
"Parrag, M.; Stansfeld, P. J. "
|
|
2741
|
+
"MemPrO: A Predictive Tool for Membrane Protein Orientation. "
|
|
2742
|
+
"J. Chem. Theory Comput. 2025. https://doi.org/10.1021/acs.jctc.5c01433."
|
|
2743
|
+
],
|
|
2744
|
+
"martini": [
|
|
2745
|
+
"Souza, P. C. T.; Alessandri, R.; Barnoud, J.; et al. "
|
|
2746
|
+
"Martini 3: a general purpose force field for coarse-grained molecular dynamics. "
|
|
2747
|
+
"Nat Methods 2021, 18, 382–388. https://doi.org/10.1038/s41592-021-01098-3."
|
|
2748
|
+
],
|
|
2749
|
+
"pdb2pqr": [
|
|
2750
|
+
"Dolinsky, T. J.; Czodrowski, P.; Li, H.; Nielsen, J. E.; Jensen, J. H.; Klebe, G.; Baker, N. A. "
|
|
2751
|
+
"PDB2PQR: Expanding and Upgrading Automated Preparation of Biomolecular Structures for Molecular Simulations. "
|
|
2752
|
+
"Nucleic Acids Res. 2007, 35 (Web Server issue), W522–W525. https://doi.org/10.1093/nar/gkm276.",
|
|
2753
|
+
"Dolinsky, T. J.; Nielsen, J. E.; McCammon, J. A.; Baker, N. A. "
|
|
2754
|
+
"PDB2PQR: An Automated Pipeline for the Setup of Poisson–Boltzmann Electrostatics Calculations. "
|
|
2755
|
+
"Nucleic Acids Res. 2004, 32 (Web Server issue), W665–W667. https://doi.org/10.1093/nar/gkh381.",
|
|
2756
|
+
],
|
|
2757
|
+
}
|
|
2758
|
+
used = [tool for tool in ("packmol-memgen", "packmol", "mempro", "martini", "pdb2pqr") if tool in self._used_tools]
|
|
2759
|
+
if not used:
|
|
2760
|
+
return
|
|
2761
|
+
tool_labels = {
|
|
2762
|
+
"packmol-memgen": "PACKMOL-Memgen",
|
|
2763
|
+
"packmol": "PACKMOL",
|
|
2764
|
+
"mempro": "MemPrO",
|
|
2765
|
+
"martini": "Martini",
|
|
2766
|
+
"pdb2pqr": "PDB2PQR",
|
|
2767
|
+
}
|
|
2768
|
+
header = "References (for tools used in this run):"
|
|
2769
|
+
logger.info(header)
|
|
2770
|
+
for tool in used:
|
|
2771
|
+
entries = references.get(tool, [])
|
|
2772
|
+
for entry in entries:
|
|
2773
|
+
label = tool_labels.get(tool, tool)
|
|
2774
|
+
line = "- [" + label + "] " + entry
|
|
2775
|
+
logger.info(line)
|
|
2776
|
+
|
|
2777
|
+
def run_all(self):
|
|
2778
|
+
pack = self.prepare()
|
|
2779
|
+
if pack:
|
|
2780
|
+
self.run_packmol()
|
|
2781
|
+
self.postprocess()
|
|
2782
|
+
self.cleanup()
|
|
2783
|
+
|
|
2784
|
+
def lipids_df(self):
|
|
2785
|
+
try:
|
|
2786
|
+
return pd.DataFrame.from_dict(self.parameters).T
|
|
2787
|
+
except:
|
|
2788
|
+
print("Run prepare() to populate the lipid parameters")
|
|
2789
|
+
|
|
2790
|
+
def solvents_df(self):
|
|
2791
|
+
try:
|
|
2792
|
+
return pd.DataFrame.from_dict(self.sparameters).T
|
|
2793
|
+
except:
|
|
2794
|
+
print("Run prepare() to populate the solvent parameters")
|
|
2795
|
+
|
|
2796
|
+
def ions_df(self):
|
|
2797
|
+
return pd.DataFrame.from_dict(amber_ion_dict).T
|
|
2798
|
+
|
|
2799
|
+
def _setup_logging(log_path):
|
|
2800
|
+
logger.setLevel(logging.DEBUG)
|
|
2801
|
+
logger.handlers = []
|
|
2802
|
+
streamer = logging.StreamHandler()
|
|
2803
|
+
streamer.setLevel(logging.INFO)
|
|
2804
|
+
loghandle = logging.FileHandler(log_path, mode="a")
|
|
2805
|
+
loghandle.setLevel(logging.DEBUG)
|
|
2806
|
+
loghandle.setFormatter(logging.Formatter('%(asctime)s:%(levelname)s:\n%(message)s',datefmt='%m/%d/%Y %I:%M:%S %p'))
|
|
2807
|
+
logger.addHandler(streamer)
|
|
2808
|
+
logger.addHandler(loghandle)
|
|
2809
|
+
|
|
2810
|
+
|
|
2811
|
+
def cli():
|
|
2812
|
+
args = parser.parse_args()
|
|
2813
|
+
outdir = os.path.abspath(args.outdir or ".")
|
|
2814
|
+
os.makedirs(outdir, exist_ok=True)
|
|
2815
|
+
if args.log and not os.path.isabs(args.log):
|
|
2816
|
+
args.log = os.path.join(outdir, args.log)
|
|
2817
|
+
if args.output and not os.path.isabs(args.output):
|
|
2818
|
+
args.output = os.path.join(outdir, args.output)
|
|
2819
|
+
if getattr(args, "packlog", None) and not os.path.isabs(args.packlog):
|
|
2820
|
+
args.packlog = os.path.join(outdir, args.packlog)
|
|
2821
|
+
_setup_logging(args.log)
|
|
2822
|
+
pmg = PACKMOLMemgen(args)
|
|
2823
|
+
pmg.run_all()
|
|
2824
|
+
|
|
2825
|
+
|
|
2826
|
+
if __name__ == "__main__":
|
|
2827
|
+
cli()
|