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.
@@ -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