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.
@@ -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,10 @@
1
+ LICENSE
2
+ README.md
3
+ center_map.py
4
+ pyproject.toml
5
+ center_map.egg-info/PKG-INFO
6
+ center_map.egg-info/SOURCES.txt
7
+ center_map.egg-info/dependency_links.txt
8
+ center_map.egg-info/requires.txt
9
+ center_map.egg-info/top_level.txt
10
+ tests/test_center_map.py
@@ -0,0 +1,5 @@
1
+ numpy
2
+
3
+ [test]
4
+ pytest
5
+ gemmi
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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')