ihmtools 0.0.1a2__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.
- ihmtools-0.0.1a2/LICENSE +21 -0
- ihmtools-0.0.1a2/MANIFEST.in +7 -0
- ihmtools-0.0.1a2/PKG-INFO +198 -0
- ihmtools-0.0.1a2/README.md +175 -0
- ihmtools-0.0.1a2/examples/README.md +43 -0
- ihmtools-0.0.1a2/examples/assemble.py +212 -0
- ihmtools-0.0.1a2/examples/data/9A9W.png +0 -0
- ihmtools-0.0.1a2/examples/data/coordinates.cif +54292 -0
- ihmtools-0.0.1a2/examples/data/crosslinks.csv +91 -0
- ihmtools-0.0.1a2/examples/data/restraints.csv +52 -0
- ihmtools-0.0.1a2/pyproject.toml +40 -0
- ihmtools-0.0.1a2/setup.cfg +4 -0
- ihmtools-0.0.1a2/src/ihmtools/__init__.py +9 -0
- ihmtools-0.0.1a2/src/ihmtools/ihmdep.py +869 -0
- ihmtools-0.0.1a2/src/ihmtools/ihmv.py +769 -0
- ihmtools-0.0.1a2/src/ihmtools.egg-info/PKG-INFO +198 -0
- ihmtools-0.0.1a2/src/ihmtools.egg-info/SOURCES.txt +21 -0
- ihmtools-0.0.1a2/src/ihmtools.egg-info/dependency_links.txt +1 -0
- ihmtools-0.0.1a2/src/ihmtools.egg-info/entry_points.txt +3 -0
- ihmtools-0.0.1a2/src/ihmtools.egg-info/requires.txt +4 -0
- ihmtools-0.0.1a2/src/ihmtools.egg-info/top_level.txt +1 -0
- ihmtools-0.0.1a2/tests/test_ihmdep.py +224 -0
- ihmtools-0.0.1a2/tests/test_ihmv.py +246 -0
ihmtools-0.0.1a2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Arthur Zalevsky
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
include README.md
|
|
2
|
+
recursive-include examples *.py *.md
|
|
3
|
+
include examples/data/coordinates.cif examples/data/crosslinks.csv
|
|
4
|
+
include examples/data/restraints.csv examples/data/9A9W.png
|
|
5
|
+
# output, local scratch, and the collection example (11 MB) stay out of the sdist
|
|
6
|
+
exclude examples/data/assembled.cif
|
|
7
|
+
prune examples/data/G_1000003
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ihmtools
|
|
3
|
+
Version: 0.0.1a2
|
|
4
|
+
Summary: Command-line tools for the PDB-IHM validation and deposition systems
|
|
5
|
+
Author-email: Arthur Zalevsky <aozalevsky@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/salilab/ihmtools
|
|
8
|
+
Project-URL: Repository, https://github.com/salilab/ihmtools
|
|
9
|
+
Project-URL: PDB-IHM, https://pdb-ihm.org
|
|
10
|
+
Keywords: pdb-ihm,deriva,ermrest,hatrac,integrative modeling
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: requests
|
|
20
|
+
Provides-Extra: test
|
|
21
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# ihmtools
|
|
25
|
+
|
|
26
|
+
Command-line tools for the [PDB-IHM](https://pdb-ihm.org) validation and
|
|
27
|
+
deposition systems. They talk to DERIVA's two REST APIs directly — ERMrest for
|
|
28
|
+
records, Hatrac for files — so the only dependency is `requests`.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install ihmtools
|
|
32
|
+
ihmv login # once; Globus, via the browser
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
While only the TestPyPI pre-release exists, the second index is not optional:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install --index-url https://test.pypi.org/simple/ \
|
|
39
|
+
--extra-index-url https://pypi.org/simple/ ihmtools
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
TestPyPI carries its own stale copy of `requests` (2.5.4.1, from 2015), so
|
|
43
|
+
without `--extra-index-url` pip installs that instead of the real one and every
|
|
44
|
+
command dies with `module 'collections' has no attribute 'MutableMapping'`.
|
|
45
|
+
|
|
46
|
+
Both commands default to the **dev** server; `--mode production` switches.
|
|
47
|
+
|
|
48
|
+
The examples below live in the repository, so clone it to run them:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
git clone https://github.com/salilab/ihmtools.git
|
|
52
|
+
cd ihmtools
|
|
53
|
+
pip install -e .
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## `ihmv` — validation catalog
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
ihmv upload model.cif submit a structure for validation
|
|
60
|
+
ihmv run model.cif upload and block until it finishes
|
|
61
|
+
ihmv get_status list entries, newest first
|
|
62
|
+
ihmv get_status 2ZJ one word plus an exit code
|
|
63
|
+
ihmv set_status 2QJ --to Reprocess ask the pipeline to run it again
|
|
64
|
+
ihmv download 2Y0 2XT fetch validation reports
|
|
65
|
+
ihmv delete 2Y0 remove a record and its reports
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## `ihmdep` — deposition system
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
ihmdep upload model.cif --image model.png deposit an entry
|
|
72
|
+
ihmdep run model.cif deposit and block
|
|
73
|
+
ihmdep get_status list entries, newest first
|
|
74
|
+
ihmdep set_status 9-DXAM --to SUBMIT DRAFT / DEPO / SUBMIT only
|
|
75
|
+
ihmdep download 9-DXAM fetch generated reports
|
|
76
|
+
ihmdep delete 9-DXAM pre-submit entries only
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Preparing an entry from raw files
|
|
80
|
+
|
|
81
|
+
`examples/` builds a depositable IHM mmCIF out of what an experimenter
|
|
82
|
+
actually has, using PDB-IHM entry **9A9W** — "USP7 bound to a nucleosome/p53
|
|
83
|
+
complex": histones, two DNA strands, p53, USP7 and four zincs, with DSSO
|
|
84
|
+
crosslinking MS and a 3DEM map.
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pip install gemmi ihm # the example needs these two; the CLIs do not
|
|
88
|
+
cd examples # from the repository root
|
|
89
|
+
|
|
90
|
+
python assemble.py # writes data/assembled.cif
|
|
91
|
+
ihmdep upload data/assembled.cif --image data/9A9W.png
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`assemble.py` reads three files and derives everything else with python-ihm —
|
|
95
|
+
entities with the right alphabet for protein, DNA and the zinc ligand, one
|
|
96
|
+
asym unit per chain copy, an atomic representation over the residues actually
|
|
97
|
+
observed, the datasets, both restraints, the protocol, and the model:
|
|
98
|
+
|
|
99
|
+
| file | what it is |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `data/coordinates.cif` | the model, with no IHM metadata at all |
|
|
102
|
+
| `data/crosslinks.csv` | `id,protein1,residue1,protein2,residue2,linker` |
|
|
103
|
+
| `data/restraints.csv` | `crosslink_id,chain1,chain2` |
|
|
104
|
+
| `data/9A9W.png` | the entry image, for the deposit |
|
|
105
|
+
|
|
106
|
+
For your own system these come from your pipeline. 9A9W's are checked in, so
|
|
107
|
+
the example runs on a fresh clone; they were recovered from the released entry
|
|
108
|
+
by stripping every `_ihm*` category from `pdb-ihm.org/cif/9A9W.cif`, taking the
|
|
109
|
+
two crosslink tables from `_ihm_cross_link_list` and
|
|
110
|
+
`_ihm_cross_link_restraint`, and fetching `pdb-ihm.org/images/9a9w.png`
|
|
111
|
+
(lowercase id).
|
|
112
|
+
|
|
113
|
+
The crosslinks are two files because mmCIF keeps them apart and so does the
|
|
114
|
+
science. `crosslinks.csv` is what the experiment measured — protein and
|
|
115
|
+
residue, with no idea which copy. `restraints.csv` is what the modelling
|
|
116
|
+
actually restrained: the chain pair, for the subset used. For 9A9W those are
|
|
117
|
+
90 and 51. Of the 40 measurements left unrestrained, 25 have a residue that
|
|
118
|
+
isn't in the coordinates — it fell in a disordered gap, so there is no atom to
|
|
119
|
+
measure to — and the copy assignment can't be recovered from a measurement at
|
|
120
|
+
all: "H2B residue 24 to H2B residue 28" doesn't say *which* H2B, and there are
|
|
121
|
+
two of each histone and four p53.
|
|
122
|
+
|
|
123
|
+
The result matches the original where it should: identical atoms and
|
|
124
|
+
sequences, both crosslink categories row for row, and it validates against
|
|
125
|
+
`mmcif_ihm.dic` + `mmcif_pdbx_v50.dic`.
|
|
126
|
+
|
|
127
|
+
## Scripting
|
|
128
|
+
|
|
129
|
+
`get_status` exits `0` done, `1` error, `2` pending, `3` unknown RID, so a
|
|
130
|
+
submit-and-wait loop is just:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
RID=$(ihmv upload model.cif)
|
|
134
|
+
ihmv get_status --wait "$RID" && ihmv download "$RID" -o reports/
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`--wait` is a flag; `--interval SECS` changes the 30-second poll. They are
|
|
138
|
+
separate because a RID can be all digits, and an option that took an optional
|
|
139
|
+
value would read `--wait 300` as an interval rather than as RID 300.
|
|
140
|
+
|
|
141
|
+
Depositing several entries works the same way. `upload` prints nothing but
|
|
142
|
+
the RID on stdout, so the loop's output is the RID list, and every later
|
|
143
|
+
command reads it back with `-`.
|
|
144
|
+
|
|
145
|
+
`examples/data/G_1000003/` holds three entries from one PDB-IHM collection —
|
|
146
|
+
9A40, 9A6P and 9A7U, from "Modelling protein complexes with crosslinking mass
|
|
147
|
+
spectrometry and deep learning" — with their coordinates and images:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
cd examples/data/G_1000003 # from the repository root
|
|
151
|
+
|
|
152
|
+
for id in 9A40 9A6P 9A7U; do
|
|
153
|
+
ihmdep upload "$id.cif" --image "$id.png"
|
|
154
|
+
done > rids.txt
|
|
155
|
+
|
|
156
|
+
ihmdep get_status --wait - < rids.txt &&
|
|
157
|
+
ihmdep set_status --to SUBMIT --yes - < rids.txt &&
|
|
158
|
+
ihmdep get_status --wait - < rids.txt &&
|
|
159
|
+
ihmdep download --mmcif -o generated/ - < rids.txt
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
A failed upload prints no RID, so it drops out of the batch rather than
|
|
163
|
+
stopping it, and re-running the loop picks up the existing RIDs instead of
|
|
164
|
+
depositing twice. `get_status` exits non-zero if any entry errored, which
|
|
165
|
+
keeps a broken batch from being submitted. After SUBMIT the generated mmCIF
|
|
166
|
+
comes first; the validation PDFs arrive later, hence `--mmcif`.
|
|
167
|
+
|
|
168
|
+
Listings are aligned on a terminal and **tab-separated when piped**, with a
|
|
169
|
+
`#`-prefixed header. Several columns contain spaces (`RECORD READY`, `Error:
|
|
170
|
+
processing uploaded mmCIF file`), so split on tabs rather than whitespace:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
ihmdep get_status | awk -F'\t' '!/^#/ && $5 ~ /^Error/ {print $1}'
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
RIDs come from arguments, from `--rid` (repeatable), or from stdin via `-`.
|
|
177
|
+
|
|
178
|
+
## Notes
|
|
179
|
+
|
|
180
|
+
The two modules are deliberately self-contained — each can be copied out and
|
|
181
|
+
run on its own — which means they duplicate their auth and HTTP layers. A fix
|
|
182
|
+
to one must be applied to both.
|
|
183
|
+
|
|
184
|
+
Uploads follow the catalog's own `tag:isrd.isi.edu,2017:asset` annotation for
|
|
185
|
+
where files go and which extensions are accepted, which is what the web UI
|
|
186
|
+
obeys. Don't substitute the `bulk-upload` annotation that `deriva-upload-cli`
|
|
187
|
+
reads: on dev it points at a different Hatrac namespace.
|
|
188
|
+
|
|
189
|
+
## Tests
|
|
190
|
+
|
|
191
|
+
From the repository root:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
pip install -e '.[test]'
|
|
195
|
+
pytest
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Offline only — no network or credentials needed.
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# ihmtools
|
|
2
|
+
|
|
3
|
+
Command-line tools for the [PDB-IHM](https://pdb-ihm.org) validation and
|
|
4
|
+
deposition systems. They talk to DERIVA's two REST APIs directly — ERMrest for
|
|
5
|
+
records, Hatrac for files — so the only dependency is `requests`.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install ihmtools
|
|
9
|
+
ihmv login # once; Globus, via the browser
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
While only the TestPyPI pre-release exists, the second index is not optional:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install --index-url https://test.pypi.org/simple/ \
|
|
16
|
+
--extra-index-url https://pypi.org/simple/ ihmtools
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
TestPyPI carries its own stale copy of `requests` (2.5.4.1, from 2015), so
|
|
20
|
+
without `--extra-index-url` pip installs that instead of the real one and every
|
|
21
|
+
command dies with `module 'collections' has no attribute 'MutableMapping'`.
|
|
22
|
+
|
|
23
|
+
Both commands default to the **dev** server; `--mode production` switches.
|
|
24
|
+
|
|
25
|
+
The examples below live in the repository, so clone it to run them:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
git clone https://github.com/salilab/ihmtools.git
|
|
29
|
+
cd ihmtools
|
|
30
|
+
pip install -e .
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## `ihmv` — validation catalog
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
ihmv upload model.cif submit a structure for validation
|
|
37
|
+
ihmv run model.cif upload and block until it finishes
|
|
38
|
+
ihmv get_status list entries, newest first
|
|
39
|
+
ihmv get_status 2ZJ one word plus an exit code
|
|
40
|
+
ihmv set_status 2QJ --to Reprocess ask the pipeline to run it again
|
|
41
|
+
ihmv download 2Y0 2XT fetch validation reports
|
|
42
|
+
ihmv delete 2Y0 remove a record and its reports
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## `ihmdep` — deposition system
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
ihmdep upload model.cif --image model.png deposit an entry
|
|
49
|
+
ihmdep run model.cif deposit and block
|
|
50
|
+
ihmdep get_status list entries, newest first
|
|
51
|
+
ihmdep set_status 9-DXAM --to SUBMIT DRAFT / DEPO / SUBMIT only
|
|
52
|
+
ihmdep download 9-DXAM fetch generated reports
|
|
53
|
+
ihmdep delete 9-DXAM pre-submit entries only
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Preparing an entry from raw files
|
|
57
|
+
|
|
58
|
+
`examples/` builds a depositable IHM mmCIF out of what an experimenter
|
|
59
|
+
actually has, using PDB-IHM entry **9A9W** — "USP7 bound to a nucleosome/p53
|
|
60
|
+
complex": histones, two DNA strands, p53, USP7 and four zincs, with DSSO
|
|
61
|
+
crosslinking MS and a 3DEM map.
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install gemmi ihm # the example needs these two; the CLIs do not
|
|
65
|
+
cd examples # from the repository root
|
|
66
|
+
|
|
67
|
+
python assemble.py # writes data/assembled.cif
|
|
68
|
+
ihmdep upload data/assembled.cif --image data/9A9W.png
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`assemble.py` reads three files and derives everything else with python-ihm —
|
|
72
|
+
entities with the right alphabet for protein, DNA and the zinc ligand, one
|
|
73
|
+
asym unit per chain copy, an atomic representation over the residues actually
|
|
74
|
+
observed, the datasets, both restraints, the protocol, and the model:
|
|
75
|
+
|
|
76
|
+
| file | what it is |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `data/coordinates.cif` | the model, with no IHM metadata at all |
|
|
79
|
+
| `data/crosslinks.csv` | `id,protein1,residue1,protein2,residue2,linker` |
|
|
80
|
+
| `data/restraints.csv` | `crosslink_id,chain1,chain2` |
|
|
81
|
+
| `data/9A9W.png` | the entry image, for the deposit |
|
|
82
|
+
|
|
83
|
+
For your own system these come from your pipeline. 9A9W's are checked in, so
|
|
84
|
+
the example runs on a fresh clone; they were recovered from the released entry
|
|
85
|
+
by stripping every `_ihm*` category from `pdb-ihm.org/cif/9A9W.cif`, taking the
|
|
86
|
+
two crosslink tables from `_ihm_cross_link_list` and
|
|
87
|
+
`_ihm_cross_link_restraint`, and fetching `pdb-ihm.org/images/9a9w.png`
|
|
88
|
+
(lowercase id).
|
|
89
|
+
|
|
90
|
+
The crosslinks are two files because mmCIF keeps them apart and so does the
|
|
91
|
+
science. `crosslinks.csv` is what the experiment measured — protein and
|
|
92
|
+
residue, with no idea which copy. `restraints.csv` is what the modelling
|
|
93
|
+
actually restrained: the chain pair, for the subset used. For 9A9W those are
|
|
94
|
+
90 and 51. Of the 40 measurements left unrestrained, 25 have a residue that
|
|
95
|
+
isn't in the coordinates — it fell in a disordered gap, so there is no atom to
|
|
96
|
+
measure to — and the copy assignment can't be recovered from a measurement at
|
|
97
|
+
all: "H2B residue 24 to H2B residue 28" doesn't say *which* H2B, and there are
|
|
98
|
+
two of each histone and four p53.
|
|
99
|
+
|
|
100
|
+
The result matches the original where it should: identical atoms and
|
|
101
|
+
sequences, both crosslink categories row for row, and it validates against
|
|
102
|
+
`mmcif_ihm.dic` + `mmcif_pdbx_v50.dic`.
|
|
103
|
+
|
|
104
|
+
## Scripting
|
|
105
|
+
|
|
106
|
+
`get_status` exits `0` done, `1` error, `2` pending, `3` unknown RID, so a
|
|
107
|
+
submit-and-wait loop is just:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
RID=$(ihmv upload model.cif)
|
|
111
|
+
ihmv get_status --wait "$RID" && ihmv download "$RID" -o reports/
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`--wait` is a flag; `--interval SECS` changes the 30-second poll. They are
|
|
115
|
+
separate because a RID can be all digits, and an option that took an optional
|
|
116
|
+
value would read `--wait 300` as an interval rather than as RID 300.
|
|
117
|
+
|
|
118
|
+
Depositing several entries works the same way. `upload` prints nothing but
|
|
119
|
+
the RID on stdout, so the loop's output is the RID list, and every later
|
|
120
|
+
command reads it back with `-`.
|
|
121
|
+
|
|
122
|
+
`examples/data/G_1000003/` holds three entries from one PDB-IHM collection —
|
|
123
|
+
9A40, 9A6P and 9A7U, from "Modelling protein complexes with crosslinking mass
|
|
124
|
+
spectrometry and deep learning" — with their coordinates and images:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
cd examples/data/G_1000003 # from the repository root
|
|
128
|
+
|
|
129
|
+
for id in 9A40 9A6P 9A7U; do
|
|
130
|
+
ihmdep upload "$id.cif" --image "$id.png"
|
|
131
|
+
done > rids.txt
|
|
132
|
+
|
|
133
|
+
ihmdep get_status --wait - < rids.txt &&
|
|
134
|
+
ihmdep set_status --to SUBMIT --yes - < rids.txt &&
|
|
135
|
+
ihmdep get_status --wait - < rids.txt &&
|
|
136
|
+
ihmdep download --mmcif -o generated/ - < rids.txt
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
A failed upload prints no RID, so it drops out of the batch rather than
|
|
140
|
+
stopping it, and re-running the loop picks up the existing RIDs instead of
|
|
141
|
+
depositing twice. `get_status` exits non-zero if any entry errored, which
|
|
142
|
+
keeps a broken batch from being submitted. After SUBMIT the generated mmCIF
|
|
143
|
+
comes first; the validation PDFs arrive later, hence `--mmcif`.
|
|
144
|
+
|
|
145
|
+
Listings are aligned on a terminal and **tab-separated when piped**, with a
|
|
146
|
+
`#`-prefixed header. Several columns contain spaces (`RECORD READY`, `Error:
|
|
147
|
+
processing uploaded mmCIF file`), so split on tabs rather than whitespace:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
ihmdep get_status | awk -F'\t' '!/^#/ && $5 ~ /^Error/ {print $1}'
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
RIDs come from arguments, from `--rid` (repeatable), or from stdin via `-`.
|
|
154
|
+
|
|
155
|
+
## Notes
|
|
156
|
+
|
|
157
|
+
The two modules are deliberately self-contained — each can be copied out and
|
|
158
|
+
run on its own — which means they duplicate their auth and HTTP layers. A fix
|
|
159
|
+
to one must be applied to both.
|
|
160
|
+
|
|
161
|
+
Uploads follow the catalog's own `tag:isrd.isi.edu,2017:asset` annotation for
|
|
162
|
+
where files go and which extensions are accepted, which is what the web UI
|
|
163
|
+
obeys. Don't substitute the `bulk-upload` annotation that `deriva-upload-cli`
|
|
164
|
+
reads: on dev it points at a different Hatrac namespace.
|
|
165
|
+
|
|
166
|
+
## Tests
|
|
167
|
+
|
|
168
|
+
From the repository root:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
pip install -e '.[test]'
|
|
172
|
+
pytest
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Offline only — no network or credentials needed.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Building a 9A9W entry
|
|
2
|
+
|
|
3
|
+
`assemble.py` turns raw files into a depositable IHM mmCIF, as a worked
|
|
4
|
+
example of preparing an entry. **See the main README for the workflow and what
|
|
5
|
+
the input files are** — this covers only what is worth knowing if you adapt
|
|
6
|
+
the code.
|
|
7
|
+
|
|
8
|
+
From the repository root:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install gemmi ihm
|
|
12
|
+
cd examples && python assemble.py
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
One entry, deliberately — not a general converter. It reads
|
|
16
|
+
`data/coordinates.cif`, `data/crosslinks.csv` and `data/restraints.csv`, and
|
|
17
|
+
writes `data/assembled.cif`. gemmi reads the coordinates; python-ihm describes
|
|
18
|
+
everything else.
|
|
19
|
+
|
|
20
|
+
The inputs are checked in, so this runs on a fresh clone. Only the output and
|
|
21
|
+
local scratch are ignored.
|
|
22
|
+
|
|
23
|
+
## Things that bite
|
|
24
|
+
|
|
25
|
+
- **`_entity.pdbx_description` is the only naming a coordinate file carries.**
|
|
26
|
+
Without it the crosslink list's protein names have nothing to match against.
|
|
27
|
+
gemmi's `make_mmcif_document()` drops it, so anything that rewrites the
|
|
28
|
+
coordinates has to put it back.
|
|
29
|
+
- **The alphabets are keyed differently.** `DNAAlphabet` uses `DA`,
|
|
30
|
+
`LPeptideAlphabet` the one-letter code. Look each component up by name
|
|
31
|
+
first, then by letter.
|
|
32
|
+
- **Non-polymers have no `label_seq`.** The zincs vanish from the model if you
|
|
33
|
+
skip atoms without one, and the write then fails with *"Assemblies reference
|
|
34
|
+
asym IDs that don't have coordinates"*. A single-component entity's `seq_id`
|
|
35
|
+
is always 1.
|
|
36
|
+
- **Ask the entity whether it is polymeric** before giving an asym unit a
|
|
37
|
+
residue range — older gemmi assigns `label_seq` to non-polymers too, and
|
|
38
|
+
`ihm` rejects a range on a ligand.
|
|
39
|
+
- **Chains have gaps.** Chain A holds 96 of its 139 residues, so the
|
|
40
|
+
representation describes what was modelled, not the full sequence.
|
|
41
|
+
|
|
42
|
+
Needs current gemmi and python-ihm (tested with gemmi 0.7.5 / ihm 2.11);
|
|
43
|
+
gemmi 0.5.8 groups entities differently and the build fails.
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Rebuild PDB-IHM entry 9A9W from raw coordinates and a crosslink list.
|
|
3
|
+
|
|
4
|
+
9A9W is "USP7 bound to a nucleosome/p53 complex": histones, two DNA strands,
|
|
5
|
+
p53, USP7 and four zincs, with DSSO crosslinking MS and a 3DEM map. It reads
|
|
6
|
+
data/coordinates.cif, data/crosslinks.csv and data/restraints.csv, and writes
|
|
7
|
+
data/assembled.cif.
|
|
8
|
+
|
|
9
|
+
gemmi reads the coordinates; python-ihm describes everything else. This is a
|
|
10
|
+
worked example for one entry, not a general converter -- the metadata below is
|
|
11
|
+
9A9W's, and the code assumes what 9A9W contains.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import csv
|
|
15
|
+
import os
|
|
16
|
+
|
|
17
|
+
import gemmi
|
|
18
|
+
|
|
19
|
+
import ihm
|
|
20
|
+
import ihm.dataset
|
|
21
|
+
import ihm.dumper
|
|
22
|
+
import ihm.location
|
|
23
|
+
import ihm.model
|
|
24
|
+
import ihm.protocol
|
|
25
|
+
import ihm.reader
|
|
26
|
+
import ihm.representation
|
|
27
|
+
import ihm.restraint
|
|
28
|
+
|
|
29
|
+
DATA = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")
|
|
30
|
+
COORDINATES = os.path.join(DATA, "coordinates.cif")
|
|
31
|
+
CROSSLINKS = os.path.join(DATA, "crosslinks.csv")
|
|
32
|
+
RESTRAINTS = os.path.join(DATA, "restraints.csv")
|
|
33
|
+
OUTPUT = os.path.join(DATA, "assembled.cif")
|
|
34
|
+
|
|
35
|
+
# --------------------------------------------------------------------------
|
|
36
|
+
# Read the coordinates. gemmi gives the sequences, the chain grouping and the
|
|
37
|
+
# entity descriptions; those descriptions are what the crosslink list names
|
|
38
|
+
# its proteins by.
|
|
39
|
+
# --------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
structure = gemmi.read_structure(COORDINATES)
|
|
42
|
+
structure.setup_entities()
|
|
43
|
+
structure.assign_label_seq_id()
|
|
44
|
+
|
|
45
|
+
block = gemmi.cif.read(COORDINATES).sole_block()
|
|
46
|
+
category = block.get_mmcif_category("_entity")
|
|
47
|
+
descriptions = dict(zip(category["id"], category["pdbx_description"]))
|
|
48
|
+
|
|
49
|
+
# --------------------------------------------------------------------------
|
|
50
|
+
# One Entity per unique sequence, one AsymUnit per chain copy.
|
|
51
|
+
#
|
|
52
|
+
# The alphabets are keyed differently -- DNAAlphabet by "DA", LPeptideAlphabet
|
|
53
|
+
# by the one-letter code -- so look each component up by name first. The four
|
|
54
|
+
# zincs are non-polymers; each needs its own single-component entity.
|
|
55
|
+
# --------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
entities = {} # gemmi entity name -> Entity
|
|
58
|
+
asyms = {} # subchain id -> AsymUnit
|
|
59
|
+
|
|
60
|
+
for gemmi_entity in structure.entities:
|
|
61
|
+
description = descriptions[gemmi_entity.name]
|
|
62
|
+
|
|
63
|
+
if gemmi_entity.entity_type == gemmi.EntityType.Polymer:
|
|
64
|
+
alphabet = (ihm.DNAAlphabet
|
|
65
|
+
if gemmi_entity.polymer_type == gemmi.PolymerType.Dna
|
|
66
|
+
else ihm.LPeptideAlphabet)
|
|
67
|
+
comps = alphabet()._comps
|
|
68
|
+
sequence = [
|
|
69
|
+
comps.get(name)
|
|
70
|
+
or comps[gemmi.find_tabulated_residue(name).one_letter_code.upper()]
|
|
71
|
+
for name in gemmi_entity.full_sequence]
|
|
72
|
+
entity = ihm.Entity(sequence, alphabet=alphabet, description=description)
|
|
73
|
+
else:
|
|
74
|
+
name = next(residue.name for chain in structure[0] for residue in chain
|
|
75
|
+
if residue.subchain in gemmi_entity.subchains)
|
|
76
|
+
entity = ihm.Entity([ihm.NonPolymerChemComp(name, name=description)],
|
|
77
|
+
description=description)
|
|
78
|
+
|
|
79
|
+
entities[gemmi_entity.name] = entity
|
|
80
|
+
for subchain in gemmi_entity.subchains:
|
|
81
|
+
asyms[subchain] = ihm.AsymUnit(entity, details=description, id=subchain)
|
|
82
|
+
|
|
83
|
+
by_description = {e.description: e for e in entities.values()}
|
|
84
|
+
by_chain = {a.id: a for a in asyms.values()}
|
|
85
|
+
|
|
86
|
+
system = ihm.System(title="USP7 bound to a nucleosome/p53 complex")
|
|
87
|
+
system.authors.extend(["Chakraborty, D.", "Kempf, G.", "Kater, L.",
|
|
88
|
+
"Cavadini, S.", "Thoma, N.H."])
|
|
89
|
+
system.entities.extend(entities.values())
|
|
90
|
+
system.asym_units.extend(asyms.values())
|
|
91
|
+
|
|
92
|
+
assembly = ihm.Assembly(list(asyms.values()), name="Modeled assembly")
|
|
93
|
+
|
|
94
|
+
# --------------------------------------------------------------------------
|
|
95
|
+
# Representation: atomic, over the residues actually present. Chains have gaps
|
|
96
|
+
# -- chain A holds 96 of its 139 residues -- so describe what was modelled
|
|
97
|
+
# rather than the whole sequence.
|
|
98
|
+
# --------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
observed = {}
|
|
101
|
+
for chain in structure[0]:
|
|
102
|
+
for residue in chain:
|
|
103
|
+
if residue.label_seq is not None:
|
|
104
|
+
observed.setdefault(residue.subchain, []).append(residue.label_seq)
|
|
105
|
+
|
|
106
|
+
# A residue range only means anything for a polymer; the zincs take the whole
|
|
107
|
+
# asym unit. (Ask the entity rather than checking for seq ids -- older gemmi
|
|
108
|
+
# assigns label_seq to non-polymers too.)
|
|
109
|
+
representation = ihm.representation.Representation([
|
|
110
|
+
ihm.representation.AtomicSegment(
|
|
111
|
+
asym(min(observed[sid]), max(observed[sid]))
|
|
112
|
+
if asym.entity.is_polymeric() else asym,
|
|
113
|
+
rigid=False)
|
|
114
|
+
for sid, asym in asyms.items()])
|
|
115
|
+
|
|
116
|
+
# --------------------------------------------------------------------------
|
|
117
|
+
# The data 9A9W was built from.
|
|
118
|
+
# --------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
crosslink_data = ihm.dataset.CXMSDataset(ihm.location.PRIDELocation("PXD054141"))
|
|
121
|
+
em_data = ihm.dataset.EMDensityDataset(ihm.location.EMDBLocation("EMD-53517"))
|
|
122
|
+
system.orphan_datasets.extend([
|
|
123
|
+
crosslink_data, em_data,
|
|
124
|
+
ihm.dataset.DeNovoModelDataset(
|
|
125
|
+
ihm.location.AlphaFoldDBLocation("AF-Q93009-F1-v4")),
|
|
126
|
+
ihm.dataset.PDBDataset(ihm.location.PDBLocation("9R04")),
|
|
127
|
+
])
|
|
128
|
+
|
|
129
|
+
# --------------------------------------------------------------------------
|
|
130
|
+
# Crosslinks come in two layers, as they do in mmCIF.
|
|
131
|
+
#
|
|
132
|
+
# crosslinks.csv is what the experiment measured: protein and residue, with no
|
|
133
|
+
# idea which copy. restraints.csv is what the modelling actually restrained --
|
|
134
|
+
# the chain pair, for the subset of measurements used. 9A9W measures 90 and
|
|
135
|
+
# restrains 51 of them; the other 40 stay in the list and restrain nothing.
|
|
136
|
+
# --------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
crosslink_restraint = ihm.restraint.CrossLinkRestraint(
|
|
139
|
+
dataset=crosslink_data, linker=ihm.ChemDescriptor("DSSO"))
|
|
140
|
+
|
|
141
|
+
experimental = {} # measurement id -> link
|
|
142
|
+
for row in csv.DictReader(open(CROSSLINKS)):
|
|
143
|
+
link = ihm.restraint.ExperimentalCrossLink(
|
|
144
|
+
by_description[row["protein1"]].residue(int(row["residue1"])),
|
|
145
|
+
by_description[row["protein2"]].residue(int(row["residue2"])))
|
|
146
|
+
experimental[row["id"]] = link
|
|
147
|
+
crosslink_restraint.experimental_cross_links.append([link])
|
|
148
|
+
|
|
149
|
+
distance = ihm.restraint.UpperBoundDistanceRestraint(30.0)
|
|
150
|
+
for row in csv.DictReader(open(RESTRAINTS)):
|
|
151
|
+
crosslink_restraint.cross_links.append(ihm.restraint.ResidueCrossLink(
|
|
152
|
+
experimental_cross_link=experimental[row["crosslink_id"]],
|
|
153
|
+
asym1=by_chain[row["chain1"]], asym2=by_chain[row["chain2"]],
|
|
154
|
+
distance=distance))
|
|
155
|
+
|
|
156
|
+
em_restraint = ihm.restraint.EM3DRestraint(
|
|
157
|
+
dataset=em_data, assembly=assembly, fitting_method="Flexible fitting")
|
|
158
|
+
system.restraints.extend([crosslink_restraint, em_restraint])
|
|
159
|
+
|
|
160
|
+
# --------------------------------------------------------------------------
|
|
161
|
+
# How the model was built.
|
|
162
|
+
# --------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
protocol = ihm.protocol.Protocol(name="Modeling")
|
|
165
|
+
for name, method in [("Structure prediction", "AlphaFold2"),
|
|
166
|
+
("Rigid body fitting", "Rigid body fitting"),
|
|
167
|
+
("Model editing", "Manual editing"),
|
|
168
|
+
("Flexible fitting", "Molecular dynamics flexible fitting"),
|
|
169
|
+
("Refinement", "Maximum-likelihood refinement")]:
|
|
170
|
+
protocol.steps.append(ihm.protocol.Step(
|
|
171
|
+
assembly=assembly, dataset_group=None, method=method, name=name,
|
|
172
|
+
num_models_begin=None, num_models_end=None))
|
|
173
|
+
system.orphan_protocols.append(protocol)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class Model(ihm.model.Model):
|
|
177
|
+
"""Streams atoms from gemmi rather than holding a second copy of them."""
|
|
178
|
+
|
|
179
|
+
def get_atoms(self):
|
|
180
|
+
for chain in structure[0]:
|
|
181
|
+
for residue in chain:
|
|
182
|
+
# Non-polymers have no label_seq, but a single-component
|
|
183
|
+
# entity's seq_id is always 1.
|
|
184
|
+
seq_id = residue.label_seq or 1
|
|
185
|
+
for atom in residue:
|
|
186
|
+
yield ihm.model.Atom(
|
|
187
|
+
asym_unit=asyms[residue.subchain], seq_id=seq_id,
|
|
188
|
+
atom_id=atom.name, type_symbol=atom.element.name,
|
|
189
|
+
x=atom.pos.x, y=atom.pos.y, z=atom.pos.z,
|
|
190
|
+
het=residue.het_flag == "H",
|
|
191
|
+
biso=atom.b_iso, occupancy=atom.occ)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
model = Model(assembly=assembly, protocol=protocol,
|
|
195
|
+
representation=representation, name="Best scoring model")
|
|
196
|
+
em_restraint.fits[model] = ihm.restraint.EM3DRestraintFit()
|
|
197
|
+
|
|
198
|
+
system.state_groups.append(ihm.model.StateGroup(
|
|
199
|
+
[ihm.model.State([ihm.model.ModelGroup([model], name="All models")])]))
|
|
200
|
+
|
|
201
|
+
with open(OUTPUT, "w") as fh:
|
|
202
|
+
ihm.dumper.write(fh, [system])
|
|
203
|
+
print("wrote %s" % OUTPUT)
|
|
204
|
+
|
|
205
|
+
# Read it back -- the cheapest check that what we wrote is well formed.
|
|
206
|
+
with open(OUTPUT) as fh:
|
|
207
|
+
check, = ihm.reader.read(fh)
|
|
208
|
+
crosslinks = check.restraints[0]
|
|
209
|
+
print("%d entities, %d restraints, %d measurements, %d restrained"
|
|
210
|
+
% (len(check.entities), len(check.restraints),
|
|
211
|
+
sum(len(g) for g in crosslinks.experimental_cross_links),
|
|
212
|
+
len(crosslinks.cross_links)))
|
|
Binary file
|