center-map 0.1.0__py3-none-any.whl
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,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,6 @@
|
|
|
1
|
+
center_map.py,sha256=23d3eHr23iEgbcPLn2pyOxuNEVVS4MAT5nFMdPwlgG4,4251
|
|
2
|
+
center_map-0.1.0.dist-info/licenses/LICENSE,sha256=uJp5CZh75pWRbDoIz7klDs9ZWA43pWr0OkcR89tLXoE,1072
|
|
3
|
+
center_map-0.1.0.dist-info/METADATA,sha256=-1RfjeZ6yYZQypowJQ_8uouXtZQp7LzWYEu1rLsAQpM,3038
|
|
4
|
+
center_map-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
5
|
+
center_map-0.1.0.dist-info/top_level.txt,sha256=U9LeT0WxiKDzc2y4EaC3jXeDuSdYlXCOK9ECfwPaVrY,11
|
|
6
|
+
center_map-0.1.0.dist-info/RECORD,,
|
|
@@ -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 @@
|
|
|
1
|
+
center_map
|
center_map.py
ADDED
|
@@ -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']
|