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 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
+ ![](./figs/image1.png) to
81
+ ![](./figs/image.png)
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
+ ![](./figs/image1.png) to
59
+ ![](./figs/image.png)
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"]