mempro 0.3.2__tar.gz → 0.3.3__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mempro
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Membrane Protein Orientation in Lipid Bilayers
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -2780,10 +2780,15 @@ def get_charge(lz,uz):
2780
2780
  pcharge = 0
2781
2781
  for ind,j in enumerate(protein.atoms):
2782
2782
  pos = protein.coord[ind]
2783
- if not j[0].strip().startswith('v') and j[1:3] != last:
2783
+ if protein_q is not None:
2784
+ if(pos[2] > lz and pos[2] < uz):
2785
+ pcharge += protein_q[ind]
2786
+ elif not j[0].strip().startswith('v') and j[1:3] != last:
2784
2787
  if(pos[2] > lz and pos[2] < uz):
2785
2788
  pcharge += charges.get(j[1].strip(),0)
2786
2789
  last = j[1:3]
2790
+ if protein_q is not None:
2791
+ pcharge = int(round(pcharge))
2787
2792
  total_charge = mcharge+pgcharge+pcharge
2788
2793
  return total_charge,pcharge,mcharge,pgcharge
2789
2794
 
@@ -3363,6 +3368,7 @@ Input/output related options
3363
3368
  ("-o", Option(str, 1, None, "Output GRO file: Membrane with Protein")),
3364
3369
  ("-o_f", Option(str, 1, None, "Folder for additional files (Default: InsaneData)")),
3365
3370
  ("-p", Option(str, 1, None, "Optional rudimentary topology file")),
3371
+ ("-p_top", Option(str, 1, None, "Topology (.top) of the protein in -f, e.g. from martinize2. Protein charges are taken from it, including the termini, instead of from residue names")),
3366
3372
  ("-ct", Option(str, 1, None, "This will create a template of the membrane only.")),
3367
3373
  ("-in_t", Option(str, 1, None, "Input template for placement of multiple proteins.")),
3368
3374
  ("-fs", Option(str, 1, None, "Input text file with multiple proteins")),
@@ -3607,17 +3613,39 @@ def read_lipid_itp(fn):
3607
3613
  if not lines:
3608
3614
  continue
3609
3615
  name = lines[0][0]
3610
- atoms, charge, sec = [], 0.0, None
3616
+ atoms, resnames, qs, sec = [], [], [], None
3611
3617
  for l in lines[1:]:
3612
3618
  if l[0].startswith("["):
3613
3619
  sec = "".join(l).strip("[]")
3614
3620
  elif sec == "atoms" and len(l) >= 7 and not l[0].startswith("#"):
3615
3621
  atoms.append(l[4])
3616
- charge += float(l[6])
3617
- mols[name] = {"atoms": atoms, "charge": int(round(charge)), "build": builds.get(name)}
3622
+ resnames.append(l[3])
3623
+ qs.append(float(l[6]))
3624
+ mols[name] = {"atoms": atoms, "resnames": resnames, "charges": qs, "charge": int(round(sum(qs))), "build": builds.get(name)}
3618
3625
  return mols
3619
3626
 
3620
3627
 
3628
+ # Charge of each bead of the molecules in a topology (.top), in order, with its residue name
3629
+ def read_top_charges(fn):
3630
+ top_dir = os.path.dirname(os.path.abspath(fn))
3631
+ with open(fn) as f:
3632
+ txt = f.read()
3633
+ mols = read_lipid_itp(fn)
3634
+ for inc in re.findall(r'^\s*#include\s+"([^"]+)"', txt, re.M):
3635
+ path = inc if os.path.isabs(inc) else os.path.join(top_dir, inc)
3636
+ if os.path.exists(path):
3637
+ mols.update(read_lipid_itp(path))
3638
+ beads = []
3639
+ for line in txt.split("[ molecules ]")[-1].splitlines() if "[ molecules ]" in txt else []:
3640
+ l = line.split(";")[0].split()
3641
+ if len(l) != 2:
3642
+ continue
3643
+ if l[0] not in mols:
3644
+ raise KeyError("molecule %s in %s has no [ atoms ] in the included itps" % (l[0], fn))
3645
+ beads += list(zip(mols[l[0]]["resnames"], mols[l[0]]["charges"]))*int(l[1])
3646
+ return beads
3647
+
3648
+
3621
3649
  # The Martini 3 lipidome, for lipids without a built-in template
3622
3650
  lipidome_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "lipidome_m3.dat")
3623
3651
  if os.path.exists(lipidome_file):
@@ -3638,9 +3666,13 @@ for fn in lipid_itps:
3638
3666
  if mol["build"]:
3639
3667
  kv = mol["build"]
3640
3668
  define_lipid(name, kv.get("alhead",""), kv.get("allink",""), kv.get("altail",""))
3641
- elif name in lipidsa and name in used_lipids:
3669
+ elif name in lipidsa:
3642
3670
  beads = [b for b in lipidsa[name][1].split() if b != "-"]
3643
- if beads != mol["atoms"]:
3671
+ if beads != mol["atoms"] and len(beads) == len(mol["atoms"]):
3672
+ # Same beads under other names (e.g. CARD in the Martini 3 lipidome): take the names from the topology
3673
+ names = iter(mol["atoms"])
3674
+ lipidsa[name] = (lipidsa[name][0], " ".join(b if b == "-" else next(names) for b in lipidsa[name][1].split()))
3675
+ elif beads != mol["atoms"] and name in used_lipids:
3644
3676
  print("WARNING: The bead names of %s in %s differ from the template (%s); the built lipid will not match the topology" % (name, fn, " ".join(beads)), file=sys.stderr)
3645
3677
 
3646
3678
  # Add specified lipid definition to insane lipid library
@@ -3730,6 +3762,7 @@ elif options["-z"].value:
3730
3762
 
3731
3763
  protein = Structure()
3732
3764
  protein_lip = Structure()
3765
+ protein_q = None # Charge of each protein bead, from -p_top
3733
3766
  prot = []
3734
3767
  xshifts = [0] # Shift in x direction per protein
3735
3768
 
@@ -4940,6 +4973,17 @@ if(Num_PGL > 0):
4940
4973
 
4941
4974
  # Charge of the system so far
4942
4975
 
4976
+ # Residue names miss charges that are not on side chains, e.g. the termini, so take them from the protein topology if given
4977
+ if options["-p_top"].value and protein:
4978
+ try:
4979
+ beads = read_top_charges(options["-p_top"].value)
4980
+ except (OSError, KeyError) as e:
4981
+ beads = []
4982
+ print("; WARNING: Could not read protein charges from %s (%s)" % (options["-p_top"].value, e), file=sys.stderr)
4983
+ if len(beads) == len(protein.atoms) and all(r[:4] == a[1].strip()[:4] for (r, _), a in zip(beads, protein.atoms)):
4984
+ protein_q = [q for _, q in beads]
4985
+ elif beads:
4986
+ print("; WARNING: The %d beads in %s do not match the %d protein beads in -f, so protein charges are inferred from residue names" % (len(beads), options["-p_top"].value, len(protein.atoms)), file=sys.stderr)
4943
4987
 
4944
4988
  charge,pcharge,mcharge,pgcharge = get_charge(0,pbcz)
4945
4989
 
@@ -3436,6 +3436,11 @@ def add_Reses(Res, Resitp):
3436
3436
  Beadtype[bt] = current_beadtype_no
3437
3437
  current_beadtype_no += 1
3438
3438
 
3439
+ if len(restobead_add) == 0:
3440
+ # Without bead types the residue cannot be placed, and load_cg_pdb would fail with an IndexError
3441
+ print("ERROR: Additional residue "+Res+" has no [ atoms ] entries in "+Resitp+". Add its definition to this itp (-res_itp).")
3442
+ sys.exit(1)
3443
+
3439
3444
  ResToBead.append(restobead_add)
3440
3445
 
3441
3446
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mempro
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Membrane Protein Orientation in Lipid Bilayers
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -16,4 +16,5 @@ mempro.egg-info/requires.txt
16
16
  mempro.egg-info/top_level.txt
17
17
  tests/test_footprint.py
18
18
  tests/test_insane4mempro.py
19
+ tests/test_residues.py
19
20
  tests/test_sort.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mempro"
7
- version = "0.3.2"
7
+ version = "0.3.3"
8
8
  description = "Membrane Protein Orientation in Lipid Bilayers"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -3,11 +3,9 @@ Tests for the per-leaflet protein footprint (-footprint map, docs/footprint-desi
3
3
 
4
4
  Run from the repository root with: python -m unittest discover tests
5
5
  The synthetic-shape tests call the footprint block directly (well under a second each); the build tests run
6
- Insane4MemPrO on small synthetic proteins (a few seconds each). The QmcA test builds a 234k-bead system and only runs
7
- with MEMPRO_SLOW_TESTS=1.
6
+ Insane4MemPrO on small synthetic proteins (a few seconds each).
8
7
  """
9
8
 
10
- import gzip
11
9
  import os
12
10
  import re
13
11
  import shutil
@@ -21,7 +19,6 @@ import numpy as np
21
19
  ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
22
20
  INSANE = os.path.join(ROOT, "mempro", "Insane4MemPrO.py")
23
21
  LEAFLETS = os.path.join(ROOT, "mempro", "leaflet_check.py")
24
- QMCA = os.path.join(ROOT, "tests", "data", "QmcA")
25
22
  sys.path.insert(0, os.path.join(ROOT, "mempro"))
26
23
  import leaflet_check # noqa: E402
27
24
 
@@ -302,27 +299,10 @@ class TestEnclosedBuild(unittest.TestCase):
302
299
  inside = heads[np.linalg.norm(heads - 5.0, axis=1) < 1.3]
303
300
  self.assertGreater(len(inside), 0)
304
301
  self.assertLess(len(inside), 10)
302
+ # Lipids start at least LIPID_MIN_SPACING (0.55 nm) apart, but the head bead sits 0.125 nm off the lipid
303
+ # axis in a random direction, plus a kick of up to 0.1 nm in x and y, so heads can come within 0.16 nm
305
304
  d = np.linalg.norm(heads[:, None] - heads[None], axis=2) + 10 * np.eye(len(heads))
306
- self.assertGreater(d.min(), 0.3)
307
-
308
-
309
- @unittest.skipUnless(os.environ.get("MEMPRO_SLOW_TESTS"), "set MEMPRO_SLOW_TESTS=1 to build QmcA (234k beads)")
310
- class TestQmcA(unittest.TestCase):
311
- def test_leaflets_matched(self):
312
- tmp = tempfile.mkdtemp(prefix="qmca_test_")
313
- TMP_DIRS.append(tmp)
314
- with gzip.open(os.path.join(QMCA, "protein-cg_rank_1.pdb.gz"), "rt") as f, open(os.path.join(tmp, "prot.pdb"), "w") as g:
315
- g.write(f.read())
316
- cmd = [sys.executable, INSANE, "-f", "prot.pdb", "-x", "32.11502", "-y", "32.262005", "-z", "24.711554",
317
- "-l", "POPE:8", "-l", "POPG:2", "-sol", "W", "-negi_c0", "CL", "-posi_c0", "NA", "-footprint", "map",
318
- "-o", "sys.gro", "-p", "topol.top"]
319
- out = subprocess.run(cmd, cwd=tmp, capture_output=True, text=True)
320
- self.assertEqual(out.returncode, 0, out.stderr[-3000:])
321
- (fp_up, _, _), (fp_lo, _, _) = map_leaflets(out.stdout + out.stderr)
322
- self.assertGreater(fp_lo, 3 * fp_up) # the soluble cap rests on the lower headgroups
323
- chk = subprocess.run([sys.executable, LEAFLETS, os.path.join(tmp, "sys.gro"), "-insane", INSANE], capture_output=True, text=True)
324
- diff = float(re.search(r"Differential strain \(upper - lower\): (\S+)%", chk.stdout).group(1))
325
- self.assertLess(abs(diff), 1.0)
305
+ self.assertGreater(d.min(), 0.15)
326
306
 
327
307
 
328
308
  if __name__ == "__main__":
@@ -217,6 +217,64 @@ class TestLipidome(unittest.TestCase):
217
217
  self.assertEqual(membrane_charge(log), -counts["XXES"])
218
218
  self.assertIn("WARNING: The bead names of POPC", log)
219
219
 
220
+ def test_lipid_itp_bead_names_by_position(self):
221
+ """Martini 3 lipidome CARD has no ;@INSANE line and renamed beads; the names come from the itp."""
222
+ tmp = tempfile.mkdtemp(prefix="insane_itp_")
223
+ TMP_DIRS.append(tmp)
224
+ itp = os.path.join(tmp, "card.itp")
225
+ beads = ("GLC PO41 GL11 GL21 C1A1 D2A1 C3A1 C4A1 C1B1 C2B1 C3B1 C4B1 "
226
+ "PO42 GL12 GL22 C1A2 D2A2 C3A2 C4A2 C1B2 C2B2 C3B2 C4B2").split()
227
+ with open(itp, "w") as f:
228
+ f.write("[ moleculetype ]\nCARD 1\n[ atoms ]\n")
229
+ for i, b in enumerate(beads):
230
+ f.write("%d C1 1 CARD %s %d %s\n" % (i + 1, b, i + 1, "-1.0" if b.startswith("PO") else "0"))
231
+ log, counts, gro = build(["-x", "10", "-y", "10", "-z", "10", "-l", "POPE:9", "-l", "CARD:1", "-lipid_itp", itp])
232
+ self.assertGreater(counts["CARD"], 0)
233
+ self.assertEqual(residue_beads(gro, "CARD"), beads)
234
+ self.assertNotIn("WARNING: The bead names of CARD", log)
235
+
236
+
237
+ class TestProteinCharge(unittest.TestCase):
238
+ """A lipoprotein chain: CYST has a neutral N-terminus, so the C-terminal -1 is not cancelled as residue names assume."""
239
+
240
+ def setUp(self):
241
+ self.dir = tempfile.mkdtemp(prefix="insane_charge_")
242
+ TMP_DIRS.append(self.dir)
243
+ resnames = ["CYST"] + ["LEU"] * 19
244
+ with open(os.path.join(self.dir, "prot.pdb"), "w") as f:
245
+ for i, r in enumerate(resnames):
246
+ f.write("ATOM %5d BB %-4sA%4d %8.3f%8.3f%8.3f 1.00 0.00\n" % (i + 1, r, i + 1, 50.0, 50.0, -28.5 + 3 * i))
247
+ f.write("END\n")
248
+ with open(os.path.join(self.dir, "chain.itp"), "w") as f:
249
+ f.write("[ moleculetype ]\nchain 1\n\n[ atoms ]\n")
250
+ for i, r in enumerate(resnames):
251
+ f.write("%d P5 %d %s BB %d %s\n" % (i + 1, i + 1, r, i + 1, "-1" if i == len(resnames) - 1 else "0.0"))
252
+
253
+ def write_top(self, copies):
254
+ top = os.path.join(self.dir, "prot.top")
255
+ with open(top, "w") as f:
256
+ f.write('#include "martini.itp"\n#include "chain.itp"\n\n[ molecules ]\nchain %d\n' % copies)
257
+ return top
258
+
259
+ def ions(self, extra):
260
+ log, counts, _ = build(["-f", os.path.join(self.dir, "prot.pdb"), "-x", "10", "-y", "10", "-z", "10", "-l", "POPE"] + extra)
261
+ return log, counts.get("NA", 0) - counts.get("CL", 0)
262
+
263
+ def test_residue_names_miss_terminus(self):
264
+ log, excess = self.ions([])
265
+ self.assertIn("Charge of protein: 0.0", log)
266
+ self.assertEqual(excess, 0)
267
+
268
+ def test_charge_from_topology(self):
269
+ log, excess = self.ions(["-p_top", self.write_top(1)])
270
+ self.assertIn("Charge of protein: -1.0", log)
271
+ self.assertEqual(excess, 1)
272
+
273
+ def test_mismatched_topology_falls_back(self):
274
+ log, excess = self.ions(["-p_top", self.write_top(2)])
275
+ self.assertIn("do not match the 20 protein beads", log)
276
+ self.assertIn("Charge of protein: 0.0", log)
277
+
220
278
 
221
279
  class TestLeafletCheck(unittest.TestCase):
222
280
  def run_check(self, gro):
@@ -0,0 +1,46 @@
1
+ """
2
+ Tests for additional residues (-res / -res_itp).
3
+
4
+ Run from the repository root with: python -m unittest discover tests
5
+ The test runs MemPrO on the QmcA CG structure and stops while the additional residues are read (a few seconds).
6
+ """
7
+
8
+ import gzip
9
+ import os
10
+ import shutil
11
+ import subprocess
12
+ import sys
13
+ import tempfile
14
+ import unittest
15
+
16
+ ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
17
+ MEMPRO = os.path.join(ROOT, "mempro", "MemPrO.py")
18
+ QMCA_CG = os.path.join(ROOT, "tests", "data", "QmcA", "QmcA-cg.pdb.gz")
19
+
20
+
21
+ class TestAdditionalResidues(unittest.TestCase):
22
+
23
+ def setUp(self):
24
+ self.tmp = tempfile.mkdtemp()
25
+ self.addCleanup(shutil.rmtree, self.tmp, ignore_errors=True)
26
+ self.cg = os.path.join(self.tmp, "QmcA-cg.pdb")
27
+ with gzip.open(QMCA_CG, "rt") as src, open(self.cg, "w") as dst:
28
+ dst.write(src.read())
29
+
30
+ def test_residue_missing_from_itp(self):
31
+ """A -res residue with no [ atoms ] entries in -res_itp stops with a clear error and a non-zero exit code."""
32
+ itp = os.path.join(self.tmp, "residues.itp")
33
+ with open(itp, "w") as f:
34
+ f.write("[ moleculetype ]\nPOPE 1\n\n[ atoms ]\n1 Q5 1 POPE NH3 1 1.0\n")
35
+
36
+ result = subprocess.run([sys.executable, MEMPRO, "-f", self.cg, "-o", os.path.join(self.tmp, "out"),
37
+ "-res", "CYST", "-res_itp", itp],
38
+ capture_output=True, text=True, timeout=600)
39
+
40
+ self.assertNotEqual(result.returncode, 0)
41
+ self.assertIn("Additional residue CYST has no [ atoms ] entries", result.stdout)
42
+ self.assertNotIn("IndexError", result.stderr)
43
+
44
+
45
+ if __name__ == "__main__":
46
+ unittest.main()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes