sabr-kit 0.4.3.dev1__tar.gz → 0.4.4__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.
- {sabr_kit-0.4.3.dev1 → sabr_kit-0.4.4}/PKG-INFO +22 -41
- {sabr_kit-0.4.3.dev1 → sabr_kit-0.4.4}/README.md +21 -39
- {sabr_kit-0.4.3.dev1 → sabr_kit-0.4.4}/pyproject.toml +0 -1
- {sabr_kit-0.4.3.dev1 → sabr_kit-0.4.4}/src/sabr/alignment.py +3 -3
- sabr_kit-0.4.4/src/sabr/api.py +159 -0
- {sabr_kit-0.4.3.dev1 → sabr_kit-0.4.4}/src/sabr/cli.py +46 -14
- {sabr_kit-0.4.3.dev1 → sabr_kit-0.4.4}/src/sabr/numbering.py +130 -88
- {sabr_kit-0.4.3.dev1 → sabr_kit-0.4.4}/src/sabr/structure.py +61 -114
- sabr_kit-0.4.3.dev1/src/sabr/api.py +0 -119
- {sabr_kit-0.4.3.dev1 → sabr_kit-0.4.4}/.gitignore +0 -0
- {sabr_kit-0.4.3.dev1 → sabr_kit-0.4.4}/LICENSE +0 -0
- {sabr_kit-0.4.3.dev1 → sabr_kit-0.4.4}/src/sabr/__init__.py +0 -0
- {sabr_kit-0.4.3.dev1 → sabr_kit-0.4.4}/src/sabr/_anarci/LICENSE +0 -0
- {sabr_kit-0.4.3.dev1 → sabr_kit-0.4.4}/src/sabr/_anarci/__init__.py +0 -0
- {sabr_kit-0.4.3.dev1 → sabr_kit-0.4.4}/src/sabr/_anarci/schemes.py +0 -0
- {sabr_kit-0.4.3.dev1 → sabr_kit-0.4.4}/src/sabr/assets/__init__.py +0 -0
- {sabr_kit-0.4.3.dev1 → sabr_kit-0.4.4}/src/sabr/assets/embeddings_noise_0.0.npz +0 -0
- {sabr_kit-0.4.3.dev1 → sabr_kit-0.4.4}/src/sabr/assets/embeddings_noise_0.2.npz +0 -0
- {sabr_kit-0.4.3.dev1 → sabr_kit-0.4.4}/src/sabr/assets/embeddings_noise_0.5.npz +0 -0
- {sabr_kit-0.4.3.dev1 → sabr_kit-0.4.4}/src/sabr/assets/embeddings_noise_1.0.npz +0 -0
- {sabr_kit-0.4.3.dev1 → sabr_kit-0.4.4}/src/sabr/assets/embeddings_noise_2.0.npz +0 -0
- {sabr_kit-0.4.3.dev1 → sabr_kit-0.4.4}/src/sabr/assets/modified_residues.json +0 -0
- {sabr_kit-0.4.3.dev1 → sabr_kit-0.4.4}/src/sabr/assets/mpnn_encoder.npz +0 -0
- {sabr_kit-0.4.3.dev1 → sabr_kit-0.4.4}/src/sabr/assets/softalign_embeddings.npz +0 -0
- {sabr_kit-0.4.3.dev1 → sabr_kit-0.4.4}/src/sabr/assets/softalign_encoder.npz +0 -0
- {sabr_kit-0.4.3.dev1 → sabr_kit-0.4.4}/src/sabr/assets/softalign_gap.npz +0 -0
- {sabr_kit-0.4.3.dev1 → sabr_kit-0.4.4}/src/sabr/constants.py +0 -0
- {sabr_kit-0.4.3.dev1 → sabr_kit-0.4.4}/src/sabr/corrections.py +0 -0
- {sabr_kit-0.4.3.dev1 → sabr_kit-0.4.4}/src/sabr/model.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sabr-kit
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.4
|
|
4
4
|
Summary: Structure-based Antibody Renumbering
|
|
5
5
|
Project-URL: Homepage, https://github.com/delalamo/sabr
|
|
6
6
|
Project-URL: Issues, https://github.com/delalamo/sabr/issues
|
|
@@ -31,7 +31,6 @@ Requires-Python: >=3.11
|
|
|
31
31
|
Requires-Dist: biopython<2,>=1.87
|
|
32
32
|
Requires-Dist: click<9,>=8.4.2
|
|
33
33
|
Requires-Dist: dm-haiku<0.1,>=0.0.16
|
|
34
|
-
Requires-Dist: gemmi<1,>=0.7.5
|
|
35
34
|
Requires-Dist: jax<0.11,>=0.10.2
|
|
36
35
|
Requires-Dist: jaxlib<0.11,>=0.10.2
|
|
37
36
|
Requires-Dist: numpy<3,>=2.4.6
|
|
@@ -78,6 +77,7 @@ sabr -i INPUT -c CHAIN -o OUTPUT
|
|
|
78
77
|
[-m sabr|softalign]
|
|
79
78
|
[--residue-range START END]
|
|
80
79
|
[--scfv]
|
|
80
|
+
[--no-mmcif]
|
|
81
81
|
[--overwrite] [-v]
|
|
82
82
|
```
|
|
83
83
|
|
|
@@ -99,13 +99,15 @@ already specifies both domain types, scFv mode requires automatic chain type.
|
|
|
99
99
|
Composite references use the selected parameter mode, so `--scfv` can be
|
|
100
100
|
combined with `--mode softalign`.
|
|
101
101
|
|
|
102
|
-
Input and output may be PDB (`.pdb`) or mmCIF (`.cif` or `.mmcif`).
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
Input and output may be PDB (`.pdb`) or mmCIF (`.cif` or `.mmcif`). When a
|
|
103
|
+
requested PDB output needs multi-character insertion codes, SAbR warns and
|
|
104
|
+
automatically writes it to the corresponding `.cif` path instead. Pass
|
|
105
|
+
`--no-mmcif` to forbid this conversion and fail. Other values that exceed PDB
|
|
106
|
+
field limits still require an explicitly named mmCIF output. Writes are atomic,
|
|
107
|
+
so a failed run does not leave a partial output.
|
|
105
108
|
|
|
106
109
|
CLI conversion guarantees preservation of atomic structure content, not
|
|
107
|
-
arbitrary non-atomic mmCIF categories. It warns for every mmCIF input.
|
|
108
|
-
those categories matter, load a Gemmi structure and use the in-memory API.
|
|
110
|
+
arbitrary non-atomic mmCIF categories. It warns for every mmCIF input.
|
|
109
111
|
|
|
110
112
|
## Python API
|
|
111
113
|
|
|
@@ -117,38 +119,19 @@ structure = PDBParser(QUIET=True).get_structure("antibody", "antibody.pdb")
|
|
|
117
119
|
numbered = renumber_structure(structure, chain="H")
|
|
118
120
|
```
|
|
119
121
|
|
|
120
|
-
|
|
122
|
+
`renumber_structure` accepts a Biopython `Structure`, never mutates its input,
|
|
123
|
+
and returns a new Biopython `Structure`. Non-target chains, hetero residues,
|
|
124
|
+
waters, and residues outside an inclusive `residue_range` are preserved. SAbR
|
|
125
|
+
rejects multi-model structures rather than silently modifying only one model.
|
|
126
|
+
If a partial range would create duplicate residue IDs with unchanged residues,
|
|
127
|
+
the operation fails with an explanation.
|
|
121
128
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
structure,
|
|
129
|
-
chain="heavy_chain",
|
|
130
|
-
scheme="chothia",
|
|
131
|
-
chain_type="auto",
|
|
132
|
-
noise_level=0.0,
|
|
133
|
-
mode="softalign",
|
|
134
|
-
residue_range=None,
|
|
135
|
-
scfv=False,
|
|
136
|
-
)
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
`renumber_structure` never mutates its input and returns the same concrete
|
|
140
|
-
structure type. Non-target chains, hetero residues, waters, and residues
|
|
141
|
-
outside an inclusive `residue_range` are preserved. SAbR rejects multi-model
|
|
142
|
-
structures rather than silently modifying only one model. If a partial range
|
|
143
|
-
would create duplicate residue IDs with unchanged residues, the operation
|
|
144
|
-
fails with an explanation.
|
|
145
|
-
|
|
146
|
-
The same-type clone preserves metadata represented by the input BioPython or
|
|
147
|
-
Gemmi object. Alternate conformers are normalized deterministically: a
|
|
148
|
-
complete blank-altloc backbone is preferred, then the complete conformer with
|
|
149
|
-
the greatest summed occupancy, with altloc name as the final tie-breaker.
|
|
150
|
-
Selections above 1,024 polymer residues are rejected before quadratic model
|
|
151
|
-
work; use `residue_range` to select the antibody domain.
|
|
129
|
+
The copy preserves metadata represented by the input Biopython object.
|
|
130
|
+
Alternate conformers are normalized deterministically: a complete blank-altloc
|
|
131
|
+
backbone is preferred, then the complete conformer with the greatest summed
|
|
132
|
+
occupancy, with altloc name as the final tie-breaker. Selections above 1,024
|
|
133
|
+
polymer residues are rejected before quadratic model work; use
|
|
134
|
+
`residue_range` to select the antibody domain.
|
|
152
135
|
|
|
153
136
|
Modified peptide residues are translated only for sequence generation. Their
|
|
154
137
|
original names and atoms remain unchanged. The committed mapping was generated
|
|
@@ -159,9 +142,7 @@ contains only peptide-linking components with exactly one canonical amino-acid
|
|
|
159
142
|
parent. Unsupported or ambiguous polymer chemistry fails explicitly; no
|
|
160
143
|
runtime network access occurs.
|
|
161
144
|
|
|
162
|
-
|
|
163
|
-
long loops that need extended codes, use a BioPython structure in memory or
|
|
164
|
-
the CLI with mmCIF output.
|
|
145
|
+
For unusually long loops that need extended insertion codes, use mmCIF output.
|
|
165
146
|
|
|
166
147
|
## Scientific behavior
|
|
167
148
|
|
|
@@ -34,6 +34,7 @@ sabr -i INPUT -c CHAIN -o OUTPUT
|
|
|
34
34
|
[-m sabr|softalign]
|
|
35
35
|
[--residue-range START END]
|
|
36
36
|
[--scfv]
|
|
37
|
+
[--no-mmcif]
|
|
37
38
|
[--overwrite] [-v]
|
|
38
39
|
```
|
|
39
40
|
|
|
@@ -55,13 +56,15 @@ already specifies both domain types, scFv mode requires automatic chain type.
|
|
|
55
56
|
Composite references use the selected parameter mode, so `--scfv` can be
|
|
56
57
|
combined with `--mode softalign`.
|
|
57
58
|
|
|
58
|
-
Input and output may be PDB (`.pdb`) or mmCIF (`.cif` or `.mmcif`).
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
Input and output may be PDB (`.pdb`) or mmCIF (`.cif` or `.mmcif`). When a
|
|
60
|
+
requested PDB output needs multi-character insertion codes, SAbR warns and
|
|
61
|
+
automatically writes it to the corresponding `.cif` path instead. Pass
|
|
62
|
+
`--no-mmcif` to forbid this conversion and fail. Other values that exceed PDB
|
|
63
|
+
field limits still require an explicitly named mmCIF output. Writes are atomic,
|
|
64
|
+
so a failed run does not leave a partial output.
|
|
61
65
|
|
|
62
66
|
CLI conversion guarantees preservation of atomic structure content, not
|
|
63
|
-
arbitrary non-atomic mmCIF categories. It warns for every mmCIF input.
|
|
64
|
-
those categories matter, load a Gemmi structure and use the in-memory API.
|
|
67
|
+
arbitrary non-atomic mmCIF categories. It warns for every mmCIF input.
|
|
65
68
|
|
|
66
69
|
## Python API
|
|
67
70
|
|
|
@@ -73,38 +76,19 @@ structure = PDBParser(QUIET=True).get_structure("antibody", "antibody.pdb")
|
|
|
73
76
|
numbered = renumber_structure(structure, chain="H")
|
|
74
77
|
```
|
|
75
78
|
|
|
76
|
-
|
|
79
|
+
`renumber_structure` accepts a Biopython `Structure`, never mutates its input,
|
|
80
|
+
and returns a new Biopython `Structure`. Non-target chains, hetero residues,
|
|
81
|
+
waters, and residues outside an inclusive `residue_range` are preserved. SAbR
|
|
82
|
+
rejects multi-model structures rather than silently modifying only one model.
|
|
83
|
+
If a partial range would create duplicate residue IDs with unchanged residues,
|
|
84
|
+
the operation fails with an explanation.
|
|
77
85
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
structure,
|
|
85
|
-
chain="heavy_chain",
|
|
86
|
-
scheme="chothia",
|
|
87
|
-
chain_type="auto",
|
|
88
|
-
noise_level=0.0,
|
|
89
|
-
mode="softalign",
|
|
90
|
-
residue_range=None,
|
|
91
|
-
scfv=False,
|
|
92
|
-
)
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
`renumber_structure` never mutates its input and returns the same concrete
|
|
96
|
-
structure type. Non-target chains, hetero residues, waters, and residues
|
|
97
|
-
outside an inclusive `residue_range` are preserved. SAbR rejects multi-model
|
|
98
|
-
structures rather than silently modifying only one model. If a partial range
|
|
99
|
-
would create duplicate residue IDs with unchanged residues, the operation
|
|
100
|
-
fails with an explanation.
|
|
101
|
-
|
|
102
|
-
The same-type clone preserves metadata represented by the input BioPython or
|
|
103
|
-
Gemmi object. Alternate conformers are normalized deterministically: a
|
|
104
|
-
complete blank-altloc backbone is preferred, then the complete conformer with
|
|
105
|
-
the greatest summed occupancy, with altloc name as the final tie-breaker.
|
|
106
|
-
Selections above 1,024 polymer residues are rejected before quadratic model
|
|
107
|
-
work; use `residue_range` to select the antibody domain.
|
|
86
|
+
The copy preserves metadata represented by the input Biopython object.
|
|
87
|
+
Alternate conformers are normalized deterministically: a complete blank-altloc
|
|
88
|
+
backbone is preferred, then the complete conformer with the greatest summed
|
|
89
|
+
occupancy, with altloc name as the final tie-breaker. Selections above 1,024
|
|
90
|
+
polymer residues are rejected before quadratic model work; use
|
|
91
|
+
`residue_range` to select the antibody domain.
|
|
108
92
|
|
|
109
93
|
Modified peptide residues are translated only for sequence generation. Their
|
|
110
94
|
original names and atoms remain unchanged. The committed mapping was generated
|
|
@@ -115,9 +99,7 @@ contains only peptide-linking components with exactly one canonical amino-acid
|
|
|
115
99
|
parent. Unsupported or ambiguous polymer chemistry fails explicitly; no
|
|
116
100
|
runtime network access occurs.
|
|
117
101
|
|
|
118
|
-
|
|
119
|
-
long loops that need extended codes, use a BioPython structure in memory or
|
|
120
|
-
the CLI with mmCIF output.
|
|
102
|
+
For unusually long loops that need extended insertion codes, use mmCIF output.
|
|
121
103
|
|
|
122
104
|
## Scientific behavior
|
|
123
105
|
|
|
@@ -162,12 +162,12 @@ def load_references(
|
|
|
162
162
|
with path.open("rb") as handle:
|
|
163
163
|
data = np.load(handle, allow_pickle=True)["arr_0"].item()
|
|
164
164
|
references = {}
|
|
165
|
-
for chain_type in
|
|
166
|
-
embeddings = np.asarray(
|
|
165
|
+
for chain_type, reference in data.items():
|
|
166
|
+
embeddings = np.asarray(reference["array"])
|
|
167
167
|
embeddings.flags.writeable = False
|
|
168
168
|
references[chain_type] = (
|
|
169
169
|
embeddings,
|
|
170
|
-
tuple(int(position) for position in
|
|
170
|
+
tuple(int(position) for position in reference["idxs"]),
|
|
171
171
|
)
|
|
172
172
|
|
|
173
173
|
if scfv:
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""The public, in-memory SAbR API."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from sabr import constants
|
|
7
|
+
from sabr.alignment import align, load_references
|
|
8
|
+
from sabr.model import encode
|
|
9
|
+
from sabr.numbering import number_alignment
|
|
10
|
+
from sabr.structure import apply_numbering, extract_chain
|
|
11
|
+
|
|
12
|
+
LOGGER = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _normalize_choice(value: str, name: str, choices: tuple[str, ...]) -> str:
|
|
16
|
+
"""Normalize a case-insensitive string constrained to known choices."""
|
|
17
|
+
if not isinstance(value, str) or value.lower() not in choices:
|
|
18
|
+
raise ValueError(f"{name} must be one of {', '.join(choices)}.")
|
|
19
|
+
return value.lower()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _normalize_chain_type(
|
|
23
|
+
chain_type: str,
|
|
24
|
+
noise_level: float,
|
|
25
|
+
mode: str,
|
|
26
|
+
) -> str:
|
|
27
|
+
if not isinstance(chain_type, str):
|
|
28
|
+
raise ValueError("chain_type must be a reference type or 'auto'.")
|
|
29
|
+
normalized = "auto" if chain_type.lower() == "auto" else chain_type.upper()
|
|
30
|
+
if normalized == "auto":
|
|
31
|
+
return normalized
|
|
32
|
+
|
|
33
|
+
reference_types = tuple(load_references(noise_level, mode))
|
|
34
|
+
if normalized not in reference_types:
|
|
35
|
+
choices = ", ".join(("auto", *reference_types))
|
|
36
|
+
raise ValueError(f"chain_type must be one of {choices}.")
|
|
37
|
+
return normalized
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _normalize_noise_level(noise_level: float) -> float:
|
|
41
|
+
if isinstance(noise_level, bool):
|
|
42
|
+
raise ValueError(
|
|
43
|
+
f"noise_level must be one of {constants.NOISE_LEVELS}."
|
|
44
|
+
)
|
|
45
|
+
try:
|
|
46
|
+
normalized_noise = float(noise_level)
|
|
47
|
+
except (TypeError, ValueError) as error:
|
|
48
|
+
raise ValueError(
|
|
49
|
+
f"noise_level must be one of {constants.NOISE_LEVELS}."
|
|
50
|
+
) from error
|
|
51
|
+
if normalized_noise not in constants.NOISE_LEVELS:
|
|
52
|
+
raise ValueError(
|
|
53
|
+
f"noise_level must be one of {constants.NOISE_LEVELS}."
|
|
54
|
+
)
|
|
55
|
+
return normalized_noise
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _validate_residue_range(
|
|
59
|
+
residue_range: tuple[int, int] | None,
|
|
60
|
+
) -> None:
|
|
61
|
+
"""Validate inclusive structure residue-number bounds.
|
|
62
|
+
|
|
63
|
+
The bounds are compared with BioPython ``residue.id[1]`` or Gemmi
|
|
64
|
+
``residue.seqid.num``. They are residue numbers from the input structure,
|
|
65
|
+
not zero-based sequence or array indices, and need not begin at one or be
|
|
66
|
+
contiguous. Every insertion-code variant sharing a selected residue number
|
|
67
|
+
is included.
|
|
68
|
+
"""
|
|
69
|
+
if residue_range is None:
|
|
70
|
+
return
|
|
71
|
+
error = "residue_range must be an inclusive (start, end) tuple."
|
|
72
|
+
if not isinstance(residue_range, tuple):
|
|
73
|
+
raise ValueError(error)
|
|
74
|
+
if len(residue_range) != 2:
|
|
75
|
+
raise ValueError(error)
|
|
76
|
+
if not all(
|
|
77
|
+
isinstance(value, int) and not isinstance(value, bool)
|
|
78
|
+
for value in residue_range
|
|
79
|
+
):
|
|
80
|
+
raise ValueError(error)
|
|
81
|
+
if residue_range[0] > residue_range[1]:
|
|
82
|
+
raise ValueError("residue_range start must not exceed its end.")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(slots=True)
|
|
86
|
+
class _RenumberOptions:
|
|
87
|
+
"""Validated and normalized arguments for one renumbering operation."""
|
|
88
|
+
|
|
89
|
+
scheme: str
|
|
90
|
+
chain_type: str
|
|
91
|
+
noise_level: float
|
|
92
|
+
residue_range: tuple[int, int] | None
|
|
93
|
+
mode: str
|
|
94
|
+
scfv: bool
|
|
95
|
+
|
|
96
|
+
def __post_init__(self) -> None:
|
|
97
|
+
self.scheme = _normalize_choice(
|
|
98
|
+
self.scheme, "scheme", constants.NUMBERING_SCHEMES
|
|
99
|
+
)
|
|
100
|
+
self.noise_level = _normalize_noise_level(self.noise_level)
|
|
101
|
+
self.mode = _normalize_choice(self.mode, "mode", constants.MODES)
|
|
102
|
+
self.chain_type = _normalize_chain_type(
|
|
103
|
+
self.chain_type,
|
|
104
|
+
self.noise_level,
|
|
105
|
+
self.mode,
|
|
106
|
+
)
|
|
107
|
+
_validate_residue_range(self.residue_range)
|
|
108
|
+
if not isinstance(self.scfv, bool):
|
|
109
|
+
raise ValueError("scfv must be a boolean.")
|
|
110
|
+
if self.scfv and self.chain_type != "auto":
|
|
111
|
+
raise ValueError("scfv requires chain_type='auto'.")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def renumber_structure(
|
|
115
|
+
structure,
|
|
116
|
+
chain: str,
|
|
117
|
+
scheme: str = "imgt",
|
|
118
|
+
chain_type: str = "auto",
|
|
119
|
+
noise_level: float = 0.0,
|
|
120
|
+
residue_range: tuple[int, int] | None = None,
|
|
121
|
+
mode: str = "sabr",
|
|
122
|
+
scfv: bool = False,
|
|
123
|
+
):
|
|
124
|
+
"""Return a renumbered copy of a Biopython structure.
|
|
125
|
+
|
|
126
|
+
``mode="softalign"`` selects the SoftAlign encoder, references, and gap
|
|
127
|
+
penalties as one scientifically consistent parameter set. ``scfv=True``
|
|
128
|
+
adds the four supported two-domain composite references.
|
|
129
|
+
"""
|
|
130
|
+
options = _RenumberOptions(
|
|
131
|
+
scheme=scheme,
|
|
132
|
+
chain_type=chain_type,
|
|
133
|
+
noise_level=noise_level,
|
|
134
|
+
residue_range=residue_range,
|
|
135
|
+
mode=mode,
|
|
136
|
+
scfv=scfv,
|
|
137
|
+
)
|
|
138
|
+
data = extract_chain(structure, chain, options.residue_range)
|
|
139
|
+
embeddings = encode(data.coords, options.mode)
|
|
140
|
+
imgt_alignment, selected_type, score = align(
|
|
141
|
+
embeddings,
|
|
142
|
+
data.gap_indices,
|
|
143
|
+
options.chain_type,
|
|
144
|
+
options.noise_level,
|
|
145
|
+
mode=options.mode,
|
|
146
|
+
scfv=options.scfv,
|
|
147
|
+
)
|
|
148
|
+
LOGGER.info(
|
|
149
|
+
"Selected %s reference with alignment score %.4f.",
|
|
150
|
+
selected_type,
|
|
151
|
+
score,
|
|
152
|
+
)
|
|
153
|
+
numbered = number_alignment(
|
|
154
|
+
imgt_alignment,
|
|
155
|
+
data.sequence,
|
|
156
|
+
options.scheme,
|
|
157
|
+
selected_type,
|
|
158
|
+
)
|
|
159
|
+
return apply_numbering(structure, chain, data, numbered)
|
|
@@ -51,6 +51,22 @@ def _validate_pdb_output(structure) -> None:
|
|
|
51
51
|
)
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
def _has_extended_insertion_codes(structure) -> bool:
|
|
55
|
+
return any(
|
|
56
|
+
len(residue.id[2].strip()) > 1 for residue in structure.get_residues()
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _resolve_output_path(structure, path: Path, no_mmcif: bool = False) -> Path:
|
|
61
|
+
if (
|
|
62
|
+
path.suffix.lower() == ".pdb"
|
|
63
|
+
and not no_mmcif
|
|
64
|
+
and _has_extended_insertion_codes(structure)
|
|
65
|
+
):
|
|
66
|
+
return path.with_suffix(".cif")
|
|
67
|
+
return path
|
|
68
|
+
|
|
69
|
+
|
|
54
70
|
def _write_structure(structure, path: Path) -> None:
|
|
55
71
|
suffix = path.suffix.lower()
|
|
56
72
|
if suffix == ".pdb":
|
|
@@ -122,6 +138,11 @@ def _write_structure(structure, path: Path) -> None:
|
|
|
122
138
|
is_flag=True,
|
|
123
139
|
help="Also try H:K, H:L, K:H, and L:H composite references.",
|
|
124
140
|
)
|
|
141
|
+
@click.option(
|
|
142
|
+
"--no-mmcif",
|
|
143
|
+
is_flag=True,
|
|
144
|
+
help="Forbid automatic mmCIF output for extended insertion codes.",
|
|
145
|
+
)
|
|
125
146
|
@click.option("--overwrite", is_flag=True)
|
|
126
147
|
@click.option("-v", "--verbose", is_flag=True)
|
|
127
148
|
def main(
|
|
@@ -134,6 +155,7 @@ def main(
|
|
|
134
155
|
mode: str,
|
|
135
156
|
residue_range: tuple | None,
|
|
136
157
|
scfv: bool,
|
|
158
|
+
no_mmcif: bool,
|
|
137
159
|
overwrite: bool,
|
|
138
160
|
verbose: bool,
|
|
139
161
|
) -> None:
|
|
@@ -143,22 +165,13 @@ def main(
|
|
|
143
165
|
format="%(levelname)s: %(message)s",
|
|
144
166
|
force=True,
|
|
145
167
|
)
|
|
146
|
-
|
|
147
|
-
raise click.ClickException(
|
|
148
|
-
f"Output already exists: {output_path}. "
|
|
149
|
-
"Use --overwrite to replace it."
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
temporary = output_path.with_name(
|
|
153
|
-
f".{output_path.stem}.{uuid.uuid4().hex}.tmp{output_path.suffix}"
|
|
154
|
-
)
|
|
168
|
+
temporary = None
|
|
155
169
|
try:
|
|
156
170
|
structure = _read_structure(input_path)
|
|
157
171
|
if input_path.suffix.lower() in (".cif", ".mmcif"):
|
|
158
172
|
LOGGER.warning(
|
|
159
|
-
"Non-atomic mmCIF categories
|
|
160
|
-
"
|
|
161
|
-
"preservation is required."
|
|
173
|
+
"Non-atomic mmCIF categories are not preserved when parsed "
|
|
174
|
+
"with Biopython."
|
|
162
175
|
)
|
|
163
176
|
LOGGER.info("JAX backend: %s", jax.default_backend())
|
|
164
177
|
result = renumber_structure(
|
|
@@ -171,12 +184,31 @@ def main(
|
|
|
171
184
|
residue_range=residue_range,
|
|
172
185
|
scfv=scfv,
|
|
173
186
|
)
|
|
187
|
+
resolved_output_path = _resolve_output_path(
|
|
188
|
+
result, output_path, no_mmcif=no_mmcif
|
|
189
|
+
)
|
|
190
|
+
if resolved_output_path.exists() and not overwrite:
|
|
191
|
+
raise click.ClickException(
|
|
192
|
+
f"Output already exists: {resolved_output_path}. "
|
|
193
|
+
"Use --overwrite to replace it."
|
|
194
|
+
)
|
|
195
|
+
if resolved_output_path != output_path:
|
|
196
|
+
LOGGER.warning(
|
|
197
|
+
"PDB cannot represent multi-character insertion codes; "
|
|
198
|
+
"automatically saving mmCIF output to %s. Use --no-mmcif "
|
|
199
|
+
"to forbid this behavior.",
|
|
200
|
+
resolved_output_path,
|
|
201
|
+
)
|
|
202
|
+
temporary = resolved_output_path.with_name(
|
|
203
|
+
f".{resolved_output_path.stem}.{uuid.uuid4().hex}"
|
|
204
|
+
f".tmp{resolved_output_path.suffix}"
|
|
205
|
+
)
|
|
174
206
|
_write_structure(result, temporary)
|
|
175
|
-
os.replace(temporary,
|
|
207
|
+
os.replace(temporary, resolved_output_path)
|
|
176
208
|
except click.ClickException:
|
|
177
209
|
raise
|
|
178
210
|
except Exception as error:
|
|
179
|
-
if temporary.exists():
|
|
211
|
+
if temporary is not None and temporary.exists():
|
|
180
212
|
try:
|
|
181
213
|
temporary.unlink()
|
|
182
214
|
except OSError as cleanup_error:
|