center-map 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.
- center_map-0.1.0/LICENSE +21 -0
- center_map-0.1.0/PKG-INFO +85 -0
- center_map-0.1.0/README.md +68 -0
- center_map-0.1.0/center_map.egg-info/PKG-INFO +85 -0
- center_map-0.1.0/center_map.egg-info/SOURCES.txt +10 -0
- center_map-0.1.0/center_map.egg-info/dependency_links.txt +1 -0
- center_map-0.1.0/center_map.egg-info/requires.txt +5 -0
- center_map-0.1.0/center_map.egg-info/top_level.txt +1 -0
- center_map-0.1.0/center_map.py +106 -0
- center_map-0.1.0/pyproject.toml +23 -0
- center_map-0.1.0/setup.cfg +4 -0
- center_map-0.1.0/tests/test_center_map.py +174 -0
center_map-0.1.0/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,85 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: center-map
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Recenter crystallographic density maps (e.g. PDBe 2fofc/fofc) on the model; a command for PyMOL
|
|
5
|
+
Author-email: Arthur Zalevsky <aozalevsky@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/aozalevsky/center-map
|
|
8
|
+
Keywords: pymol,electron density,crystallography,ccp4,map
|
|
9
|
+
Requires-Python: >=3.6
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: numpy
|
|
13
|
+
Provides-Extra: test
|
|
14
|
+
Requires-Dist: pytest; extra == "test"
|
|
15
|
+
Requires-Dist: gemmi; extra == "test"
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# center-map
|
|
19
|
+
|
|
20
|
+
`center_map` recenters crystallographic density maps on the model, the way
|
|
21
|
+
the old EDS/RCSB DSN6 maps were. It is a command for PyMOL.
|
|
22
|
+
|
|
23
|
+
Maps from `fetch 3tj3, type=2fofc` (PDBe) cover one unit cell starting at
|
|
24
|
+
the origin, so much of the model sits outside them. `center_map` cuts a new
|
|
25
|
+
box around the selection out of the periodic crystal.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
Into the Python environment the viewer runs in:
|
|
30
|
+
|
|
31
|
+
pip install center-map
|
|
32
|
+
|
|
33
|
+
then add one line to `~/.pymolrc`:
|
|
34
|
+
|
|
35
|
+
import center_map
|
|
36
|
+
|
|
37
|
+
Without pip (e.g. bundled installers): *Plugin > Plugin Manager > Install
|
|
38
|
+
New Plugin* and choose `center_map.py`. The only dependency is numpy,
|
|
39
|
+
which is already bundled.
|
|
40
|
+
|
|
41
|
+
## Use
|
|
42
|
+
|
|
43
|
+
fetch 3tj3
|
|
44
|
+
fetch 3tj3, type=2fofc
|
|
45
|
+
center_map # all maps, all atoms, 5 A margin
|
|
46
|
+
isomesh mesh, 3tj3_2fofc, 1.0
|
|
47
|
+
|
|
48
|
+
center_map 3tj3_2fofc, chain A # again, on one chain of one map
|
|
49
|
+
|
|
50
|
+
`center_map` replaces the map object in place; make meshes after it. A
|
|
51
|
+
centered map can be centered again on atoms inside its box; for a bigger
|
|
52
|
+
box, fetch the map again. `help center_map` lists the arguments.
|
|
53
|
+
|
|
54
|
+
## Why the density is exact
|
|
55
|
+
|
|
56
|
+
* Crystal density is periodic, so a map covering one full unit cell already
|
|
57
|
+
holds every value: grid point *i* is grid point *i* mod *N*. The box is
|
|
58
|
+
filled by copying grid values; nothing is interpolated or resampled, also
|
|
59
|
+
for non-orthogonal cells.
|
|
60
|
+
* The original contour scale is kept. Maps are normally rescaled on load to
|
|
61
|
+
the sigma of the loaded grid, which for a box around the protein differs
|
|
62
|
+
from the sigma of the cell; that is switched off while the box is loaded.
|
|
63
|
+
* It refuses maps it cannot handle exactly: maps that do not cover a full
|
|
64
|
+
unit cell (e.g. asymmetric-unit maps from CCP4 `fft`), and maps whose cell
|
|
65
|
+
differs from the selection's (cryo-EM maps, maps made with `map_new`,
|
|
66
|
+
other crystal forms).
|
|
67
|
+
|
|
68
|
+
## Limitations
|
|
69
|
+
|
|
70
|
+
One map state; object matrices (moved objects) are ignored.
|
|
71
|
+
|
|
72
|
+
## Tests
|
|
73
|
+
|
|
74
|
+
pip install pytest gemmi
|
|
75
|
+
python -m pytest tests # downloads a few PDBe maps on the first run
|
|
76
|
+
|
|
77
|
+
gemmi is used only by the tests, as an independent reference: the boxes
|
|
78
|
+
must match gemmi's periodic lookup bit for bit (P 21 21 21, P 1, P 21 and
|
|
79
|
+
I 2 2 2 entries), land on the original map when the viewer places them,
|
|
80
|
+
and cover every atom plus the margin.
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT. PyMOL is a trademark of Schrödinger, LLC. This project is not
|
|
85
|
+
affiliated with or endorsed by Schrödinger.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# center-map
|
|
2
|
+
|
|
3
|
+
`center_map` recenters crystallographic density maps on the model, the way
|
|
4
|
+
the old EDS/RCSB DSN6 maps were. It is a command for PyMOL.
|
|
5
|
+
|
|
6
|
+
Maps from `fetch 3tj3, type=2fofc` (PDBe) cover one unit cell starting at
|
|
7
|
+
the origin, so much of the model sits outside them. `center_map` cuts a new
|
|
8
|
+
box around the selection out of the periodic crystal.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
Into the Python environment the viewer runs in:
|
|
13
|
+
|
|
14
|
+
pip install center-map
|
|
15
|
+
|
|
16
|
+
then add one line to `~/.pymolrc`:
|
|
17
|
+
|
|
18
|
+
import center_map
|
|
19
|
+
|
|
20
|
+
Without pip (e.g. bundled installers): *Plugin > Plugin Manager > Install
|
|
21
|
+
New Plugin* and choose `center_map.py`. The only dependency is numpy,
|
|
22
|
+
which is already bundled.
|
|
23
|
+
|
|
24
|
+
## Use
|
|
25
|
+
|
|
26
|
+
fetch 3tj3
|
|
27
|
+
fetch 3tj3, type=2fofc
|
|
28
|
+
center_map # all maps, all atoms, 5 A margin
|
|
29
|
+
isomesh mesh, 3tj3_2fofc, 1.0
|
|
30
|
+
|
|
31
|
+
center_map 3tj3_2fofc, chain A # again, on one chain of one map
|
|
32
|
+
|
|
33
|
+
`center_map` replaces the map object in place; make meshes after it. A
|
|
34
|
+
centered map can be centered again on atoms inside its box; for a bigger
|
|
35
|
+
box, fetch the map again. `help center_map` lists the arguments.
|
|
36
|
+
|
|
37
|
+
## Why the density is exact
|
|
38
|
+
|
|
39
|
+
* Crystal density is periodic, so a map covering one full unit cell already
|
|
40
|
+
holds every value: grid point *i* is grid point *i* mod *N*. The box is
|
|
41
|
+
filled by copying grid values; nothing is interpolated or resampled, also
|
|
42
|
+
for non-orthogonal cells.
|
|
43
|
+
* The original contour scale is kept. Maps are normally rescaled on load to
|
|
44
|
+
the sigma of the loaded grid, which for a box around the protein differs
|
|
45
|
+
from the sigma of the cell; that is switched off while the box is loaded.
|
|
46
|
+
* It refuses maps it cannot handle exactly: maps that do not cover a full
|
|
47
|
+
unit cell (e.g. asymmetric-unit maps from CCP4 `fft`), and maps whose cell
|
|
48
|
+
differs from the selection's (cryo-EM maps, maps made with `map_new`,
|
|
49
|
+
other crystal forms).
|
|
50
|
+
|
|
51
|
+
## Limitations
|
|
52
|
+
|
|
53
|
+
One map state; object matrices (moved objects) are ignored.
|
|
54
|
+
|
|
55
|
+
## Tests
|
|
56
|
+
|
|
57
|
+
pip install pytest gemmi
|
|
58
|
+
python -m pytest tests # downloads a few PDBe maps on the first run
|
|
59
|
+
|
|
60
|
+
gemmi is used only by the tests, as an independent reference: the boxes
|
|
61
|
+
must match gemmi's periodic lookup bit for bit (P 21 21 21, P 1, P 21 and
|
|
62
|
+
I 2 2 2 entries), land on the original map when the viewer places them,
|
|
63
|
+
and cover every atom plus the margin.
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
MIT. PyMOL is a trademark of Schrödinger, LLC. This project is not
|
|
68
|
+
affiliated with or endorsed by Schrödinger.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: center-map
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Recenter crystallographic density maps (e.g. PDBe 2fofc/fofc) on the model; a command for PyMOL
|
|
5
|
+
Author-email: Arthur Zalevsky <aozalevsky@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/aozalevsky/center-map
|
|
8
|
+
Keywords: pymol,electron density,crystallography,ccp4,map
|
|
9
|
+
Requires-Python: >=3.6
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: numpy
|
|
13
|
+
Provides-Extra: test
|
|
14
|
+
Requires-Dist: pytest; extra == "test"
|
|
15
|
+
Requires-Dist: gemmi; extra == "test"
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# center-map
|
|
19
|
+
|
|
20
|
+
`center_map` recenters crystallographic density maps on the model, the way
|
|
21
|
+
the old EDS/RCSB DSN6 maps were. It is a command for PyMOL.
|
|
22
|
+
|
|
23
|
+
Maps from `fetch 3tj3, type=2fofc` (PDBe) cover one unit cell starting at
|
|
24
|
+
the origin, so much of the model sits outside them. `center_map` cuts a new
|
|
25
|
+
box around the selection out of the periodic crystal.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
Into the Python environment the viewer runs in:
|
|
30
|
+
|
|
31
|
+
pip install center-map
|
|
32
|
+
|
|
33
|
+
then add one line to `~/.pymolrc`:
|
|
34
|
+
|
|
35
|
+
import center_map
|
|
36
|
+
|
|
37
|
+
Without pip (e.g. bundled installers): *Plugin > Plugin Manager > Install
|
|
38
|
+
New Plugin* and choose `center_map.py`. The only dependency is numpy,
|
|
39
|
+
which is already bundled.
|
|
40
|
+
|
|
41
|
+
## Use
|
|
42
|
+
|
|
43
|
+
fetch 3tj3
|
|
44
|
+
fetch 3tj3, type=2fofc
|
|
45
|
+
center_map # all maps, all atoms, 5 A margin
|
|
46
|
+
isomesh mesh, 3tj3_2fofc, 1.0
|
|
47
|
+
|
|
48
|
+
center_map 3tj3_2fofc, chain A # again, on one chain of one map
|
|
49
|
+
|
|
50
|
+
`center_map` replaces the map object in place; make meshes after it. A
|
|
51
|
+
centered map can be centered again on atoms inside its box; for a bigger
|
|
52
|
+
box, fetch the map again. `help center_map` lists the arguments.
|
|
53
|
+
|
|
54
|
+
## Why the density is exact
|
|
55
|
+
|
|
56
|
+
* Crystal density is periodic, so a map covering one full unit cell already
|
|
57
|
+
holds every value: grid point *i* is grid point *i* mod *N*. The box is
|
|
58
|
+
filled by copying grid values; nothing is interpolated or resampled, also
|
|
59
|
+
for non-orthogonal cells.
|
|
60
|
+
* The original contour scale is kept. Maps are normally rescaled on load to
|
|
61
|
+
the sigma of the loaded grid, which for a box around the protein differs
|
|
62
|
+
from the sigma of the cell; that is switched off while the box is loaded.
|
|
63
|
+
* It refuses maps it cannot handle exactly: maps that do not cover a full
|
|
64
|
+
unit cell (e.g. asymmetric-unit maps from CCP4 `fft`), and maps whose cell
|
|
65
|
+
differs from the selection's (cryo-EM maps, maps made with `map_new`,
|
|
66
|
+
other crystal forms).
|
|
67
|
+
|
|
68
|
+
## Limitations
|
|
69
|
+
|
|
70
|
+
One map state; object matrices (moved objects) are ignored.
|
|
71
|
+
|
|
72
|
+
## Tests
|
|
73
|
+
|
|
74
|
+
pip install pytest gemmi
|
|
75
|
+
python -m pytest tests # downloads a few PDBe maps on the first run
|
|
76
|
+
|
|
77
|
+
gemmi is used only by the tests, as an independent reference: the boxes
|
|
78
|
+
must match gemmi's periodic lookup bit for bit (P 21 21 21, P 1, P 21 and
|
|
79
|
+
I 2 2 2 entries), land on the original map when the viewer places them,
|
|
80
|
+
and cover every atom plus the margin.
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT. PyMOL is a trademark of Schrödinger, LLC. This project is not
|
|
85
|
+
affiliated with or endorsed by Schrödinger.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
center_map
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""center_map: recenter crystallographic maps on a selection.
|
|
2
|
+
|
|
3
|
+
pip install center-map
|
|
4
|
+
echo "import center_map" >> ~/.pymolrc
|
|
5
|
+
"""
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from pymol import cmd, CmdException
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def center_map(map='', selection='all', margin=5.0, quiet=1, _self=cmd):
|
|
14
|
+
'''
|
|
15
|
+
DESCRIPTION
|
|
16
|
+
|
|
17
|
+
Recenter crystallographic map(s) on a selection, like the old EDS/RCSB
|
|
18
|
+
DSN6 maps did. Maps from "fetch code, type=2fofc" cover one unit cell
|
|
19
|
+
starting at the origin, so the model often sits partly outside them.
|
|
20
|
+
|
|
21
|
+
The map must cover a full unit cell of the selection's crystal, or
|
|
22
|
+
already contain the new box (e.g. centering again on fewer atoms).
|
|
23
|
+
Values are copied from the periodic lattice, not interpolated, and keep
|
|
24
|
+
the contour scale of the original map.
|
|
25
|
+
|
|
26
|
+
USAGE
|
|
27
|
+
|
|
28
|
+
center_map [ map [, selection [, margin ]]]
|
|
29
|
+
|
|
30
|
+
ARGUMENTS
|
|
31
|
+
|
|
32
|
+
map = str: map object {default: all maps}
|
|
33
|
+
|
|
34
|
+
selection = str: atoms the map should cover {default: all}
|
|
35
|
+
|
|
36
|
+
margin = float: padding around the atoms, in Angstrom {default: 5}
|
|
37
|
+
|
|
38
|
+
EXAMPLE
|
|
39
|
+
|
|
40
|
+
fetch 3tj3
|
|
41
|
+
fetch 3tj3, type=2fofc
|
|
42
|
+
center_map
|
|
43
|
+
isomesh mesh, 3tj3_2fofc, 1.0
|
|
44
|
+
'''
|
|
45
|
+
margin, quiet = float(margin), int(quiet)
|
|
46
|
+
xyz = _self.get_coords(selection)
|
|
47
|
+
sym = _self.get_symmetry(selection)
|
|
48
|
+
if xyz is None or not sym:
|
|
49
|
+
raise CmdException('"%s" has no atoms or no unit cell' % selection)
|
|
50
|
+
for name in [map] if map else _self.get_names_of_type('object:map'):
|
|
51
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
52
|
+
path = os.path.join(tmp, 'map.ccp4')
|
|
53
|
+
_self.save(path, name)
|
|
54
|
+
with open(path, 'rb') as f:
|
|
55
|
+
raw = f.read()
|
|
56
|
+
h = np.frombuffer(raw, np.int32, 256).copy() # CCP4 header words
|
|
57
|
+
cell = h[10:16].view(np.float32)
|
|
58
|
+
if not np.allclose(cell, sym[:6], rtol=1e-3):
|
|
59
|
+
raise CmdException('%s: unit cell differs from "%s"' % (name, selection))
|
|
60
|
+
n, start, axes = h[0:3], h[4:7], h[16:19] - 1 # column, row, section
|
|
61
|
+
N = h[7:10][axes] # cell sampling along the file axes
|
|
62
|
+
data = np.frombuffer(raw, np.float32, int(n.prod()), 1024 + h[23]).reshape(n[::-1])
|
|
63
|
+
|
|
64
|
+
# box in grid units around the atoms; margin along each axis is
|
|
65
|
+
# margin * |reciprocal axis|, i.e. measured perpendicular to the cell faces
|
|
66
|
+
a, b, c = cell[:3]
|
|
67
|
+
ca, cb, cg = np.cos(np.radians(cell[3:]))
|
|
68
|
+
sg = np.sqrt(1 - cg * cg)
|
|
69
|
+
v = np.sqrt(1 - ca * ca - cb * cb - cg * cg + 2 * ca * cb * cg)
|
|
70
|
+
frac = np.linalg.inv([[a, b * cg, c * cb],
|
|
71
|
+
[0, b * sg, c * (ca - cb * cg) / sg],
|
|
72
|
+
[0, 0, c * v / sg]])[axes]
|
|
73
|
+
f = xyz @ frac.T
|
|
74
|
+
pad = margin * np.linalg.norm(frac, axis=1)
|
|
75
|
+
lo = np.floor((f.min(0) - pad) * N).astype(np.int32)
|
|
76
|
+
hi = np.ceil((f.max(0) + pad) * N).astype(np.int32) + 1
|
|
77
|
+
|
|
78
|
+
# crystal density is periodic: grid point i is grid point i mod N; that
|
|
79
|
+
# needs a full cell along an axis, unless the box lies inside the map
|
|
80
|
+
if h[3] != 2 or not ((n >= N) | ((lo >= start) & (hi <= start + n))).all():
|
|
81
|
+
raise CmdException('%s does not cover a full unit cell, nor the box '
|
|
82
|
+
'around "%s"' % (name, selection))
|
|
83
|
+
idx = [(np.arange(l, u) - s) % m for l, u, s, m in zip(lo, hi, start, N)]
|
|
84
|
+
box = data[np.ix_(*idx[::-1])]
|
|
85
|
+
|
|
86
|
+
h[0:3], h[4:7], h[23] = hi - lo, lo, 0
|
|
87
|
+
tmpname = _self.get_unused_name('_center_map')
|
|
88
|
+
normalize = _self.get('normalize_ccp4_maps')
|
|
89
|
+
_self.set('normalize_ccp4_maps', 0) # sigma of the box != sigma of the cell
|
|
90
|
+
try:
|
|
91
|
+
_self.load_raw(h.tobytes() + box.tobytes(), 'ccp4', tmpname, quiet=1)
|
|
92
|
+
finally:
|
|
93
|
+
_self.set('normalize_ccp4_maps', normalize)
|
|
94
|
+
_self.delete(name)
|
|
95
|
+
_self.set_name(tmpname, name)
|
|
96
|
+
if not quiet:
|
|
97
|
+
print(' center_map: %s covers "%s" + %g A' % (name, selection, margin))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def __init_plugin__(app=None):
|
|
101
|
+
pass # lets the Plugin Manager install this file; the command is registered on import
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
cmd.extend('center_map', center_map)
|
|
105
|
+
cmd.auto_arg[0]['center_map'] = cmd.auto_arg[0]['map_double']
|
|
106
|
+
cmd.auto_arg[1]['center_map'] = cmd.auto_arg[1]['map_trim']
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "center-map"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Recenter crystallographic density maps (e.g. PDBe 2fofc/fofc) on the model; a command for PyMOL"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
authors = [{name = "Arthur Zalevsky", email = "aozalevsky@gmail.com"}]
|
|
12
|
+
requires-python = ">=3.6"
|
|
13
|
+
dependencies = ["numpy"] # the viewer itself is not installed from PyPI
|
|
14
|
+
keywords = ["pymol", "electron density", "crystallography", "ccp4", "map"]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Repository = "https://github.com/aozalevsky/center-map"
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
test = ["pytest", "gemmi"]
|
|
21
|
+
|
|
22
|
+
[tool.setuptools]
|
|
23
|
+
py-modules = ["center_map"]
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Tests for center_map. Need network on first run (maps are cached in .pytest_cache).
|
|
2
|
+
|
|
3
|
+
gemmi is used only here, as an independent reference implementation.
|
|
4
|
+
"""
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pytest
|
|
7
|
+
from pymol import cmd, CmdException
|
|
8
|
+
|
|
9
|
+
from center_map import center_map # importing registers the PyMOL command
|
|
10
|
+
|
|
11
|
+
gemmi = pytest.importorskip('gemmi')
|
|
12
|
+
|
|
13
|
+
SITE = '3tj3 and chain A and resi 100-110'
|
|
14
|
+
|
|
15
|
+
ENTRIES = {
|
|
16
|
+
'3tj3': 'P 21 21 21, model sticks out of the cell',
|
|
17
|
+
'2vb1': 'P 1, triclinic',
|
|
18
|
+
'1ejg': 'P 21, monoclinic',
|
|
19
|
+
'1tqn': 'I 2 2 2, centred',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture(scope='session')
|
|
24
|
+
def fetch_dir(pytestconfig):
|
|
25
|
+
return str(pytestconfig.cache.mkdir('pdbe'))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def fetch(fetch_dir, code, *types):
|
|
29
|
+
cmd.reinitialize()
|
|
30
|
+
cmd.set('fetch_path', fetch_dir)
|
|
31
|
+
for t in ('cif',) + types:
|
|
32
|
+
if cmd.fetch(code, type=t, async_=0) == -1:
|
|
33
|
+
pytest.skip('cannot fetch %s %s' % (code, t))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def as_gemmi(name, tmp_path):
|
|
37
|
+
path = str(tmp_path / (name + '.ccp4'))
|
|
38
|
+
cmd.save(path, name)
|
|
39
|
+
return gemmi.read_ccp4_map(path)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def box_xyz(m):
|
|
43
|
+
"""Cell sampling, box start and box data of an exported map, in x,y,z order."""
|
|
44
|
+
N = np.array([m.header_i32(8 + i) for i in range(3)])
|
|
45
|
+
start = np.rint(np.array(m.get_extent().minimum.tolist()) * N).astype(int)
|
|
46
|
+
m.setup(float('nan'), gemmi.MapSetup.ReorderOnly)
|
|
47
|
+
return N, start, np.array(m.grid, copy=True)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def far_corner(ref, code):
|
|
51
|
+
"""Pseudoatom at lattice point (2, -3, 1): a box around it wraps on every axis."""
|
|
52
|
+
corner = ref.grid.unit_cell.orthogonalize(gemmi.Fractional(2, -3, 1))
|
|
53
|
+
cmd.pseudoatom('spot', pos=corner.tolist())
|
|
54
|
+
cmd.set_symmetry('spot', *cmd.get_symmetry(code))
|
|
55
|
+
return 'spot'
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@pytest.mark.parametrize('code', ENTRIES)
|
|
59
|
+
@pytest.mark.parametrize('where', ['model', 'far_corner'])
|
|
60
|
+
def test_values_identical_to_gemmi_periodic_lookup(code, where, fetch_dir, tmp_path):
|
|
61
|
+
fetch(fetch_dir, code, '2fofc')
|
|
62
|
+
ref = as_gemmi(code + '_2fofc', tmp_path)
|
|
63
|
+
ref.setup(float('nan'))
|
|
64
|
+
assert not np.isnan(np.array(ref.grid)).any()
|
|
65
|
+
selection = far_corner(ref, code) if where == 'far_corner' else code
|
|
66
|
+
|
|
67
|
+
center_map(code + '_2fofc', selection)
|
|
68
|
+
|
|
69
|
+
N, start, ours = box_xyz(as_gemmi(code + '_2fofc', tmp_path))
|
|
70
|
+
assert ours.shape != tuple(N) or start.any(), 'map was not moved'
|
|
71
|
+
expected = ref.grid.get_subarray(start.tolist(), list(ours.shape))
|
|
72
|
+
assert np.array_equal(ours, expected) # bit-identical, so no renormalization either
|
|
73
|
+
assert cmd.get_setting_boolean('normalize_ccp4_maps')
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@pytest.mark.parametrize('code', ENTRIES)
|
|
77
|
+
def test_recentering_an_already_centered_map(code, fetch_dir, tmp_path):
|
|
78
|
+
fetch(fetch_dir, code, '2fofc')
|
|
79
|
+
ref = as_gemmi(code + '_2fofc', tmp_path)
|
|
80
|
+
ref.setup(float('nan'))
|
|
81
|
+
# a box wider than the cell on every axis, no longer starting at the origin
|
|
82
|
+
center_map(code + '_2fofc', 'first (%s and q > 0)' % code, 0.55 * max(cmd.get_symmetry(code)[:3]))
|
|
83
|
+
N, start, data = box_xyz(as_gemmi(code + '_2fofc', tmp_path))
|
|
84
|
+
assert start.any() and (np.array(data.shape) >= N).all()
|
|
85
|
+
|
|
86
|
+
center_map(code + '_2fofc', far_corner(ref, code))
|
|
87
|
+
|
|
88
|
+
_, start, ours = box_xyz(as_gemmi(code + '_2fofc', tmp_path))
|
|
89
|
+
assert np.array_equal(ours, ref.grid.get_subarray(start.tolist(), list(ours.shape)))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_centering_again_inside_a_centered_map(fetch_dir, tmp_path):
|
|
93
|
+
fetch(fetch_dir, '3tj3', '2fofc')
|
|
94
|
+
ref = as_gemmi('3tj3_2fofc', tmp_path)
|
|
95
|
+
ref.setup(float('nan'))
|
|
96
|
+
center_map('3tj3_2fofc', '3tj3', 5) # box narrower than the cell along x and z
|
|
97
|
+
|
|
98
|
+
center_map('3tj3_2fofc', SITE, 3)
|
|
99
|
+
|
|
100
|
+
_, start, ours = box_xyz(as_gemmi('3tj3_2fofc', tmp_path))
|
|
101
|
+
assert np.array_equal(ours, ref.grid.get_subarray(start.tolist(), list(ours.shape)))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@pytest.mark.parametrize('code', ENTRIES)
|
|
105
|
+
def test_box_covers_every_atom_plus_margin(code, fetch_dir, tmp_path):
|
|
106
|
+
fetch(fetch_dir, code, '2fofc')
|
|
107
|
+
margin = 3.0
|
|
108
|
+
center_map(code + '_2fofc', code, margin)
|
|
109
|
+
|
|
110
|
+
m = as_gemmi(code + '_2fofc', tmp_path)
|
|
111
|
+
cell = m.grid.unit_cell
|
|
112
|
+
N, start, data = box_xyz(m)
|
|
113
|
+
frac = np.array([cell.fractionalize(gemmi.Position(*p)).tolist()
|
|
114
|
+
for p in cmd.get_coords(code)])
|
|
115
|
+
rec = cell.reciprocal()
|
|
116
|
+
pad = margin * np.array([rec.a, rec.b, rec.c])
|
|
117
|
+
assert ((frac - pad) * N >= start).all()
|
|
118
|
+
assert ((frac + pad) * N <= start + np.array(data.shape) - 1).all()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@pytest.mark.parametrize('code', ENTRIES)
|
|
122
|
+
def test_pymol_places_centered_map_on_original(code, fetch_dir, tmp_path):
|
|
123
|
+
fetch(fetch_dir, code, '2fofc')
|
|
124
|
+
cmd.copy('orig', code + '_2fofc')
|
|
125
|
+
before = cmd.get_volume_field('orig')
|
|
126
|
+
|
|
127
|
+
center_map(code + '_2fofc', code)
|
|
128
|
+
_, start, data = box_xyz(as_gemmi(code + '_2fofc', tmp_path))
|
|
129
|
+
assert data.shape != before.shape or start.any(), 'map was not moved'
|
|
130
|
+
|
|
131
|
+
# PyMOL interpolates the centered map at the original (in-cell) grid nodes;
|
|
132
|
+
# exact up to float32 rounding, while a 1-voxel shift would differ by ~1 sigma
|
|
133
|
+
cmd.map_set('orig', 'copy', code + '_2fofc')
|
|
134
|
+
after = cmd.get_volume_field('orig')
|
|
135
|
+
inner = [(np.arange(n) > s) & (np.arange(n) < s + d - 1)
|
|
136
|
+
for n, s, d in zip(before.shape, start, data.shape)]
|
|
137
|
+
mask = np.ix_(*inner)
|
|
138
|
+
assert before[mask].size > 1000
|
|
139
|
+
np.testing.assert_allclose(after[mask], before[mask], rtol=0, atol=2e-3)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_default_centers_all_maps_on_all_atoms(fetch_dir):
|
|
143
|
+
fetch(fetch_dir, '3tj3', '2fofc', 'fofc')
|
|
144
|
+
cmd.do('center_map')
|
|
145
|
+
lo, hi = np.array(cmd.get_extent('3tj3'))
|
|
146
|
+
for name in ('3tj3_2fofc', '3tj3_fofc'):
|
|
147
|
+
mlo, mhi = np.array(cmd.get_extent(name))
|
|
148
|
+
assert (mlo <= lo - 5).all() and (mhi >= hi + 5).all(), name
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_refuses_map_not_covering_full_cell(fetch_dir):
|
|
152
|
+
fetch(fetch_dir, '3tj3', '2fofc')
|
|
153
|
+
inside = '3tj3 and x > 20 and y > 10 and z > 10' # map_trim ignores atoms off the map
|
|
154
|
+
assert cmd.count_atoms(inside)
|
|
155
|
+
cmd.map_trim('3tj3_2fofc', inside, 2.0)
|
|
156
|
+
assert cmd.get_volume_field('3tj3_2fofc').size < 64 * 80 * 108
|
|
157
|
+
with pytest.raises(CmdException, match='unit cell'):
|
|
158
|
+
center_map('3tj3_2fofc', '3tj3')
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_refuses_map_from_another_cell(fetch_dir):
|
|
162
|
+
fetch(fetch_dir, '3tj3')
|
|
163
|
+
if cmd.fetch('2vb1', type='2fofc', async_=0) == -1:
|
|
164
|
+
pytest.skip('cannot fetch 2vb1 2fofc')
|
|
165
|
+
with pytest.raises(CmdException, match='cell'):
|
|
166
|
+
center_map('2vb1_2fofc', '3tj3')
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_refuses_map_computed_in_pymol(fetch_dir):
|
|
170
|
+
fetch(fetch_dir, '3tj3')
|
|
171
|
+
cmd.map_new('gmap', 'gaussian', 1.0, SITE, 2)
|
|
172
|
+
assert 'gmap' in cmd.get_names()
|
|
173
|
+
with pytest.raises(CmdException, match='cell'):
|
|
174
|
+
center_map('gmap', '3tj3')
|