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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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']