pdb-md 0.1.0__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.
- pdb_md-0.1.0/LICENSE +32 -0
- pdb_md-0.1.0/PKG-INFO +81 -0
- pdb_md-0.1.0/README.md +59 -0
- pdb_md-0.1.0/figs/image.png +0 -0
- pdb_md-0.1.0/figs/image1.png +0 -0
- pdb_md-0.1.0/pdb_md/MultiChainSelector.py +55 -0
- pdb_md-0.1.0/pdb_md/SingleChainSelector.py +102 -0
- pdb_md-0.1.0/pdb_md/__init__.py +0 -0
- pdb_md-0.1.0/pdb_md/cli.py +98 -0
- pdb_md-0.1.0/pdb_md/utils.py +34 -0
- pdb_md-0.1.0/pyproject.toml +43 -0
pdb_md-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2002, Thomas Hamelryck
|
|
4
|
+
Copyright (c) 2026, Joe Hoye Dow
|
|
5
|
+
|
|
6
|
+
All rights reserved.
|
|
7
|
+
|
|
8
|
+
Redistribution and use in source and binary forms, with or without
|
|
9
|
+
modification, are permitted provided that the following conditions are met:
|
|
10
|
+
|
|
11
|
+
1. Redistributions of source code must retain the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer.
|
|
13
|
+
|
|
14
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
15
|
+
this list of conditions and the following disclaimer in the documentation
|
|
16
|
+
and/or other materials provided with the distribution.
|
|
17
|
+
|
|
18
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
19
|
+
contributors may be used to endorse or promote products derived from
|
|
20
|
+
this software without specific prior written permission.
|
|
21
|
+
|
|
22
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
23
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
24
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
25
|
+
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
26
|
+
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
27
|
+
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
28
|
+
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
29
|
+
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
30
|
+
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
31
|
+
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
32
|
+
POSSIBILITY OF SUCH DAMAGE.
|
pdb_md-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pdb-md
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pre- and post-processing toolkit for PDB/MMCIF structure files in molecular dynamics workflows.
|
|
5
|
+
Project-URL: Homepage, https://github.com/MaybeBio/PDB-MD-Suite
|
|
6
|
+
Project-URL: Issues, https://github.com/MaybeBio/PDB-MD-Suite/issues
|
|
7
|
+
Author-email: Joe Hoye Dow <luxunisgod123@gmail.com>
|
|
8
|
+
License-Expression: BSD-3-Clause
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Requires-Dist: biopython>=1.87
|
|
20
|
+
Requires-Dist: typer>=0.27.1
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# 🔬 PDB/MMCIF segment selector
|
|
24
|
+
|
|
25
|
+
> Formerly published on PyPI as `pdb-select`. That name is deprecated — use `pdb-md`.
|
|
26
|
+
|
|
27
|
+
> PDB file segmentation tool, extract specific chains and residue ranges from PDB/MMCIF files.
|
|
28
|
+
|
|
29
|
+
# Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install pdb-md
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
# Usage
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pdb-md --help
|
|
39
|
+
|
|
40
|
+
Usage: pdb-md [OPTIONS] COMMAND [ARGS]...
|
|
41
|
+
|
|
42
|
+
Segment selector for PDB/MMCIF Structure file
|
|
43
|
+
|
|
44
|
+
╭─ Options ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
|
45
|
+
│ --install-completion Install completion for the current shell. │
|
|
46
|
+
│ --show-completion Show completion for the current shell, to copy it or customize the installation. │
|
|
47
|
+
│ --help Show this message and exit. │
|
|
48
|
+
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
|
49
|
+
╭─ Commands ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
|
50
|
+
│ select Select segments from a PDB or MMCIF structure file. │
|
|
51
|
+
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
> ⚠️ Warning: Sequence index matching to Uniprot only be tested in AlphaFold PDB files.
|
|
55
|
+
>
|
|
56
|
+
> Check for SIFTS
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pdb-md select --help
|
|
60
|
+
|
|
61
|
+
Usage: pdb-md select [OPTIONS]
|
|
62
|
+
|
|
63
|
+
Select segments from a PDB or MMCIF structure file.
|
|
64
|
+
|
|
65
|
+
╭─ Options ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
|
66
|
+
│ * --input -I <path> Input PDB or MMCIF file [required] │
|
|
67
|
+
│ --output -O <path> Output PDB file, defaults to <input stem>_selected.pdb in the current working directory │
|
|
68
|
+
│ * --segment -s <str> Segments to select, either 'chain' (whole chain, e.g. A) or 'chain:start-end' (e.g. A:1-10). Repeatable. [required] │
|
|
69
|
+
│ --help Show this message and exit. │
|
|
70
|
+
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
example:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pdb-md select -I znf263_all_hs7_30_0.54_0.37_model_0.cif -O znf263_all_hs7_30_0.54_0.37_model_0_selected.pdb -s A:369-683 -s B -s C -s D -s E -s F -s G -s H -s I -s J -s K:1-47 -s L:29-75
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
change from
|
|
80
|
+
 to
|
|
81
|
+

|
pdb_md-0.1.0/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# 🔬 PDB/MMCIF segment selector
|
|
2
|
+
|
|
3
|
+
> Formerly published on PyPI as `pdb-select`. That name is deprecated — use `pdb-md`.
|
|
4
|
+
|
|
5
|
+
> PDB file segmentation tool, extract specific chains and residue ranges from PDB/MMCIF files.
|
|
6
|
+
|
|
7
|
+
# Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install pdb-md
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
# Usage
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pdb-md --help
|
|
17
|
+
|
|
18
|
+
Usage: pdb-md [OPTIONS] COMMAND [ARGS]...
|
|
19
|
+
|
|
20
|
+
Segment selector for PDB/MMCIF Structure file
|
|
21
|
+
|
|
22
|
+
╭─ Options ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
|
23
|
+
│ --install-completion Install completion for the current shell. │
|
|
24
|
+
│ --show-completion Show completion for the current shell, to copy it or customize the installation. │
|
|
25
|
+
│ --help Show this message and exit. │
|
|
26
|
+
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
|
27
|
+
╭─ Commands ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
|
28
|
+
│ select Select segments from a PDB or MMCIF structure file. │
|
|
29
|
+
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
> ⚠️ Warning: Sequence index matching to Uniprot only be tested in AlphaFold PDB files.
|
|
33
|
+
>
|
|
34
|
+
> Check for SIFTS
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pdb-md select --help
|
|
38
|
+
|
|
39
|
+
Usage: pdb-md select [OPTIONS]
|
|
40
|
+
|
|
41
|
+
Select segments from a PDB or MMCIF structure file.
|
|
42
|
+
|
|
43
|
+
╭─ Options ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
|
44
|
+
│ * --input -I <path> Input PDB or MMCIF file [required] │
|
|
45
|
+
│ --output -O <path> Output PDB file, defaults to <input stem>_selected.pdb in the current working directory │
|
|
46
|
+
│ * --segment -s <str> Segments to select, either 'chain' (whole chain, e.g. A) or 'chain:start-end' (e.g. A:1-10). Repeatable. [required] │
|
|
47
|
+
│ --help Show this message and exit. │
|
|
48
|
+
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
example:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pdb-md select -I znf263_all_hs7_30_0.54_0.37_model_0.cif -O znf263_all_hs7_30_0.54_0.37_model_0_selected.pdb -s A:369-683 -s B -s C -s D -s E -s F -s G -s H -s I -s J -s K:1-47 -s L:29-75
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
change from
|
|
58
|
+
 to
|
|
59
|
+

|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from .SingleChainSelector import SingleChainSelector
|
|
2
|
+
|
|
3
|
+
class MultiChainSelector:
|
|
4
|
+
"""Only accepts residues with right chainid, between start and end.
|
|
5
|
+
|
|
6
|
+
Remove hydrogens, waters and ligands. Only use model 0 by default.
|
|
7
|
+
|
|
8
|
+
collector for multiple SingleChainSelector objects, to be used with PDBIO
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, selectors: list[SingleChainSelector], model_id: int = 0):
|
|
12
|
+
"""Initialize the class."""
|
|
13
|
+
# chain id must be uniquely provided in one specific selector
|
|
14
|
+
ids = [selector.chain_id for selector in selectors]
|
|
15
|
+
if len(ids) != len(set(ids)):
|
|
16
|
+
raise ValueError(f"Duplicate chain_id in selectors: {ids}")
|
|
17
|
+
self.selectors = selectors
|
|
18
|
+
self.model_id = model_id
|
|
19
|
+
def accept_model(self, model):
|
|
20
|
+
"""Verify if model match the model identifier."""
|
|
21
|
+
# model - only keep model 0
|
|
22
|
+
if model.get_id() == self.model_id:
|
|
23
|
+
return 1
|
|
24
|
+
return 0
|
|
25
|
+
def accept_chain(self, chain):
|
|
26
|
+
"""Verify if chain match the chain identifier."""
|
|
27
|
+
# every chain in the structure is accepted only if it is accepted by any of the selectors (by its chain selector function)
|
|
28
|
+
for selector in self.selectors:
|
|
29
|
+
# or
|
|
30
|
+
# if chain.get_id() == selector.chain_id:
|
|
31
|
+
if selector.accept_chain(chain):
|
|
32
|
+
return 1
|
|
33
|
+
return 0
|
|
34
|
+
|
|
35
|
+
def accept_residue(self, residue):
|
|
36
|
+
"""Verify if a residue sequence is between the start and end sequence of a specific selector."""
|
|
37
|
+
# residue - between start and end
|
|
38
|
+
for selector in self.selectors:
|
|
39
|
+
# eg. selector is [SingleChainSelector(chain_id='A', regions=[(1, 10), (20, 30)], model_id=0),
|
|
40
|
+
# SingleChainSelector(chain_id='B', regions=[(5, 15)], model_id=0)]
|
|
41
|
+
# then, for residue 5-10 whether in A or B, it will be accepted but lose the information of which selector accepted it
|
|
42
|
+
# so, only the selector with the matching chain id of the residue has the right to accept the residue or not
|
|
43
|
+
if selector.chain_id == residue.get_parent().get_id():
|
|
44
|
+
return selector.accept_residue(residue)
|
|
45
|
+
return 0
|
|
46
|
+
|
|
47
|
+
def accept_atom(self, atom):
|
|
48
|
+
"""Verify if atoms are not Hydrogen."""
|
|
49
|
+
# we can also define it specifically for each selector
|
|
50
|
+
# only the selector with the matching chain id of the atom's parent residue has the right to accept the atom or not
|
|
51
|
+
# atoms - get rid of hydrogens
|
|
52
|
+
for selector in self.selectors:
|
|
53
|
+
if selector.chain_id == atom.get_parent().get_parent().get_id():
|
|
54
|
+
return selector.accept_atom(atom)
|
|
55
|
+
return 0
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# this script is adapted and modified from https://github.com/biopython/biopython/blob/master/Bio/PDB/Dice.py
|
|
2
|
+
|
|
3
|
+
# Copyright (C) 2002, Thomas Hamelryck (thamelry@binf.ku.dk)
|
|
4
|
+
#
|
|
5
|
+
# This file is part of the Biopython distribution and governed by your
|
|
6
|
+
# choice of the "Biopython License Agreement" or the "BSD 3-Clause License".
|
|
7
|
+
# Please see the LICENSE file that should have been included as part of this
|
|
8
|
+
# package.
|
|
9
|
+
#
|
|
10
|
+
# Modifications for pdb-md: Copyright (C) 2026, Joe Hoye Dow
|
|
11
|
+
# Licensed under the BSD 3-Clause License (see LICENSE).
|
|
12
|
+
|
|
13
|
+
"""Code for chopping up (dicing) a structure.
|
|
14
|
+
|
|
15
|
+
This module is used internally by the Bio.PDB.extract() function.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
import warnings
|
|
20
|
+
|
|
21
|
+
from Bio import BiopythonWarning
|
|
22
|
+
from Bio.PDB.PDBIO import PDBIO
|
|
23
|
+
from .utils import consolidate_ranges
|
|
24
|
+
|
|
25
|
+
_hydrogen = re.compile("[123 ]*H.*")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SingleChainSelector:
|
|
29
|
+
"""Only accepts residues with right chainid, between start and end.
|
|
30
|
+
|
|
31
|
+
Remove hydrogens, waters and ligands. Only use model 0 by default.
|
|
32
|
+
|
|
33
|
+
Modified to support multiple regions selected.
|
|
34
|
+
|
|
35
|
+
Pass regions=None to keep the whole chain instead of specific ranges.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
chain_id,
|
|
41
|
+
regions: list[tuple[int, int]] | None = None,
|
|
42
|
+
model_id=0,
|
|
43
|
+
):
|
|
44
|
+
"""Initialize the class.
|
|
45
|
+
|
|
46
|
+
A regions of None means "whole chain" and an empty list means "keep
|
|
47
|
+
nothing"; the two stay distinguishable because consolidate_ranges would
|
|
48
|
+
collapse both to an empty list otherwise.
|
|
49
|
+
"""
|
|
50
|
+
self.chain_id = chain_id
|
|
51
|
+
self.regions = None if regions is None else consolidate_ranges(regions)
|
|
52
|
+
self.model_id = model_id
|
|
53
|
+
|
|
54
|
+
def accept_model(self, model):
|
|
55
|
+
"""Verify if model match the model identifier."""
|
|
56
|
+
# model - only keep model 0
|
|
57
|
+
if model.get_id() == self.model_id:
|
|
58
|
+
return 1
|
|
59
|
+
return 0
|
|
60
|
+
|
|
61
|
+
def accept_chain(self, chain):
|
|
62
|
+
"""Verify if chain match chain identifier."""
|
|
63
|
+
if chain.get_id() == self.chain_id:
|
|
64
|
+
return 1
|
|
65
|
+
return 0
|
|
66
|
+
|
|
67
|
+
def accept_residue(self, residue):
|
|
68
|
+
"""Verify if a residue sequence is between the start and end sequence."""
|
|
69
|
+
# residue - between start and end
|
|
70
|
+
hetatm_flag, resseq, icode = residue.get_id()
|
|
71
|
+
if hetatm_flag != " ":
|
|
72
|
+
# skip HETATMS
|
|
73
|
+
return 0
|
|
74
|
+
if icode != " ":
|
|
75
|
+
warnings.warn(
|
|
76
|
+
f"WARNING: Icode {icode} at position {resseq}", BiopythonWarning
|
|
77
|
+
)
|
|
78
|
+
# whole chain: every standard residue is accepted
|
|
79
|
+
if self.regions is None:
|
|
80
|
+
return 1
|
|
81
|
+
# or if any(start <= resseq <= end for start, end in self.regions)
|
|
82
|
+
for start, end in self.regions:
|
|
83
|
+
if start <= resseq <= end:
|
|
84
|
+
return 1
|
|
85
|
+
return 0
|
|
86
|
+
|
|
87
|
+
def accept_atom(self, atom):
|
|
88
|
+
"""Verify if atoms are not Hydrogen."""
|
|
89
|
+
# atoms - get rid of hydrogens
|
|
90
|
+
name = atom.get_id()
|
|
91
|
+
if _hydrogen.match(name):
|
|
92
|
+
return 0
|
|
93
|
+
else:
|
|
94
|
+
return 1
|
|
95
|
+
|
|
96
|
+
# extract function only works for a single chain, useless now that we have MultiChainSelector
|
|
97
|
+
def extract(structure, chain_id, regions, filename):
|
|
98
|
+
"""Write out selected portion to filename."""
|
|
99
|
+
sel = SingleChainSelector(chain_id, regions)
|
|
100
|
+
io = PDBIO()
|
|
101
|
+
io.set_structure(structure)
|
|
102
|
+
io.save(filename, sel)
|
|
File without changes
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from Bio.PDB import MMCIFParser, PDBIO, PDBParser
|
|
7
|
+
|
|
8
|
+
from .MultiChainSelector import MultiChainSelector
|
|
9
|
+
from .SingleChainSelector import SingleChainSelector
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(help="Segment selector for PDB/MMCIF Structure file", no_args_is_help=True)
|
|
12
|
+
|
|
13
|
+
# Rules for typer: When the app registers only one command and has no callback, this command will be directly promoted to the root command (single-command collapse), and the subcommand layer will no longer exist.
|
|
14
|
+
# Adding @app.callback() explicitly declares "this is a command group"
|
|
15
|
+
@app.callback()
|
|
16
|
+
def main():
|
|
17
|
+
"""Segment selector for PDB/MMCIF structure files."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.command("select", no_args_is_help=True)
|
|
21
|
+
def select_cmd(
|
|
22
|
+
input_file: Path = typer.Option(..., "--input", "-I", help="Input PDB or MMCIF file"),
|
|
23
|
+
output_file: Optional[Path] = typer.Option(
|
|
24
|
+
None,
|
|
25
|
+
"--output",
|
|
26
|
+
"-O",
|
|
27
|
+
help="Output PDB file, defaults to <input stem>_selected.pdb in the current working directory",
|
|
28
|
+
),
|
|
29
|
+
segments: list[str] = typer.Option(
|
|
30
|
+
...,
|
|
31
|
+
"--segment",
|
|
32
|
+
"-s",
|
|
33
|
+
help="Segments to select, either 'chain' (whole chain, e.g. A) or 'chain:start-end' (e.g. A:1-10). Repeatable.",
|
|
34
|
+
),
|
|
35
|
+
):
|
|
36
|
+
"""Select segments from a PDB or MMCIF structure file."""
|
|
37
|
+
if not input_file.exists():
|
|
38
|
+
raise ValueError(f"Input file {input_file} does not exist.")
|
|
39
|
+
|
|
40
|
+
# Determine the parser based on file extension
|
|
41
|
+
suffix = input_file.suffix.lower()
|
|
42
|
+
if suffix == ".pdb":
|
|
43
|
+
parser = PDBParser()
|
|
44
|
+
elif suffix == ".cif":
|
|
45
|
+
parser = MMCIFParser()
|
|
46
|
+
else:
|
|
47
|
+
raise ValueError("Unsupported file format. Please provide a .pdb or .cif file.")
|
|
48
|
+
|
|
49
|
+
# Parse the structure
|
|
50
|
+
structure = parser.get_structure("structure", input_file)
|
|
51
|
+
|
|
52
|
+
# Create SingleChainSelector objects; a segment without a colon selects a whole chain
|
|
53
|
+
selectors = []
|
|
54
|
+
for seg in segments:
|
|
55
|
+
if ":" not in seg:
|
|
56
|
+
chain_id = seg.strip()
|
|
57
|
+
if not chain_id:
|
|
58
|
+
raise ValueError(
|
|
59
|
+
f"Invalid segment: {seg!r}. Expected 'chain' or 'chain:start-end'."
|
|
60
|
+
)
|
|
61
|
+
selectors.append(SingleChainSelector(chain_id, None))
|
|
62
|
+
continue
|
|
63
|
+
chain_id, range_str = seg.split(":", 1)
|
|
64
|
+
match = re.fullmatch(r"(\d+)-(\d+)", range_str)
|
|
65
|
+
if not match:
|
|
66
|
+
raise ValueError(f"Invalid range format: {range_str}. Expected format is start-end.")
|
|
67
|
+
start, end = map(int, match.groups())
|
|
68
|
+
selectors.append(SingleChainSelector(chain_id, [(start, end)]))
|
|
69
|
+
|
|
70
|
+
# Warn about chain ids that are absent from the model that gets written (model 0),
|
|
71
|
+
# so a typo does not silently produce an empty output file
|
|
72
|
+
existing_chains = {
|
|
73
|
+
chain.get_id()
|
|
74
|
+
for model in structure.get_models()
|
|
75
|
+
if model.get_id() == 0
|
|
76
|
+
for chain in model
|
|
77
|
+
}
|
|
78
|
+
for selector in selectors:
|
|
79
|
+
if selector.chain_id not in existing_chains:
|
|
80
|
+
typer.echo(
|
|
81
|
+
f"WARNING: chain {selector.chain_id} not found in {input_file}", err=True
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Create MultiChainSelector
|
|
85
|
+
multi_selector = MultiChainSelector(selectors)
|
|
86
|
+
|
|
87
|
+
# Write out selected portion to output file, defaulting to <input stem>_selected.pdb
|
|
88
|
+
if not output_file:
|
|
89
|
+
output_file = Path(input_file.stem + "_selected.pdb")
|
|
90
|
+
io = PDBIO()
|
|
91
|
+
io.set_structure(structure)
|
|
92
|
+
# PDBIO.save only accepts a filename string or an open filehandle, not a Path
|
|
93
|
+
io.save(str(output_file), multi_selector)
|
|
94
|
+
typer.echo(f"Wrote {output_file}")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__":
|
|
98
|
+
app()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# some utility functions defined
|
|
2
|
+
|
|
3
|
+
def consolidate_ranges(ranges: list[tuple[int, int]]) -> list[tuple[int, int]]:
|
|
4
|
+
"""
|
|
5
|
+
Description
|
|
6
|
+
-----------
|
|
7
|
+
Consolidate a list of tuple ranges into a list of non-overlapping ranges, and sort in ascending order
|
|
8
|
+
|
|
9
|
+
Args
|
|
10
|
+
----
|
|
11
|
+
ranges: list[tuple[int, int]]
|
|
12
|
+
A list of tuple ranges, e.g. [(1, 5), (3, 7), (10, 12)]
|
|
13
|
+
|
|
14
|
+
Returns
|
|
15
|
+
-------
|
|
16
|
+
list[tuple[int, int]]: A list of non-overlapping ranges, sorted in ascending order
|
|
17
|
+
|
|
18
|
+
"""
|
|
19
|
+
if not ranges:
|
|
20
|
+
return []
|
|
21
|
+
# first sort the ranges by the start value in ascending order
|
|
22
|
+
ranges.sort(key=lambda x: x[0])
|
|
23
|
+
# start with the first range
|
|
24
|
+
consolidated = [ranges[0]]
|
|
25
|
+
# start comparison
|
|
26
|
+
for current in ranges[1:]:
|
|
27
|
+
previous = consolidated[-1]
|
|
28
|
+
if current[0] <= previous[1] + 1:
|
|
29
|
+
previous[1] = max(previous[1], current[1])
|
|
30
|
+
else:
|
|
31
|
+
consolidated.append(current)
|
|
32
|
+
return consolidated
|
|
33
|
+
|
|
34
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pdb-md"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{name = "Joe Hoye Dow", email = "luxunisgod123@gmail.com"}
|
|
10
|
+
]
|
|
11
|
+
description = "Pre- and post-processing toolkit for PDB/MMCIF structure files in molecular dynamics workflows."
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.9"
|
|
14
|
+
license = "BSD-3-Clause"
|
|
15
|
+
license-files = ["LICENSE"]
|
|
16
|
+
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Programming Language :: Python :: 3.14",
|
|
25
|
+
"Operating System :: OS Independent"
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# use pip show to check
|
|
30
|
+
dependencies = [
|
|
31
|
+
"biopython>=1.87",
|
|
32
|
+
"typer>=0.27.1",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/MaybeBio/PDB-MD-Suite"
|
|
37
|
+
Issues = "https://github.com/MaybeBio/PDB-MD-Suite/issues"
|
|
38
|
+
|
|
39
|
+
[project.scripts]
|
|
40
|
+
pdb-md = "pdb_md.cli:app"
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.wheel]
|
|
43
|
+
packages = ["pdb_md"]
|