emdbva 0.0.1.dev152__tar.gz → 0.0.1.dev155__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.
Files changed (58) hide show
  1. {emdbva-0.0.1.dev152/emdbva.egg-info → emdbva-0.0.1.dev155}/PKG-INFO +1 -1
  2. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155/emdbva.egg-info}/PKG-INFO +1 -1
  3. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/emdbva.egg-info/SOURCES.txt +2 -0
  4. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/mainva.py +12 -7
  5. emdbva-0.0.1.dev155/va/metrics/phenix_locres.py +324 -0
  6. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/metrics/residue_locres.py +86 -1
  7. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/metrics/resmap.py +7 -4
  8. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/preparation.py +4 -1
  9. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/utils/ChimeraxViews.py +20 -20
  10. emdbva-0.0.1.dev155/va/utils/chimerax_combined_locres.py +80 -0
  11. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/validationanalysis.py +97 -0
  12. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/version.py +1 -1
  13. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/LICENSE +0 -0
  14. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/MANIFEST.in +0 -0
  15. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/README.rst +0 -0
  16. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/emdbva.egg-info/dependency_links.txt +0 -0
  17. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/emdbva.egg-info/entry_points.txt +0 -0
  18. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/emdbva.egg-info/requires.txt +0 -0
  19. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/emdbva.egg-info/top_level.txt +0 -0
  20. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/setup.cfg +0 -0
  21. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/setup.py +0 -0
  22. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/PATHS_codon_airflow.py +0 -0
  23. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/__init__.py +0 -0
  24. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/audit/__init__.py +0 -0
  25. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/audit/cif_revision.py +0 -0
  26. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/metrics/__init__.py +0 -0
  27. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/metrics/bars.py +0 -0
  28. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/metrics/connected_percentage.py +0 -0
  29. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/metrics/contour_level_predicator.py +0 -0
  30. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/metrics/emda_mmcc.py +0 -0
  31. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/metrics/emringer.py +0 -0
  32. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/metrics/inclusion.py +0 -0
  33. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/metrics/map_data_validation.py +0 -0
  34. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/metrics/map_map_cc.py +0 -0
  35. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/metrics/overlap_percentage.py +0 -0
  36. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/metrics/phaserandomization.py +0 -0
  37. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/metrics/phenix_cc.py +0 -0
  38. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/metrics/phenix_mm.py +0 -0
  39. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/metrics/projections.py +0 -0
  40. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/metrics/qscore.py +0 -0
  41. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/metrics/smoc.py +0 -0
  42. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/metrics/strudel.py +0 -0
  43. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/metrics/surfaces.py +0 -0
  44. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/metrics/threedfsc.py +0 -0
  45. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/prepareandrun_codon_airflow.py +0 -0
  46. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/qscores.csv +0 -0
  47. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/schemas/__init__.py +0 -0
  48. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/schemas/emdb_entry_full.schema.json +0 -0
  49. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/schemas/loader.py +0 -0
  50. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/utils/Checker.py +0 -0
  51. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/utils/MapProcessor.py +0 -0
  52. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/utils/Model.py +0 -0
  53. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/utils/__init__.py +0 -0
  54. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/utils/cl_weights.pth +0 -0
  55. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/utils/log_utils.py +0 -0
  56. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/utils/misc.py +0 -0
  57. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/utils/rescolor.py +0 -0
  58. {emdbva-0.0.1.dev152 → emdbva-0.0.1.dev155}/va/utils/stars.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: emdbva
3
- Version: 0.0.1.dev152
3
+ Version: 0.0.1.dev155
4
4
  Summary: CryoEM validation toolkit
5
5
  Home-page: https://test.pypi.org/project/va/
6
6
  Author: Zhe Wang
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: emdbva
3
- Version: 0.0.1.dev152
3
+ Version: 0.0.1.dev155
4
4
  Summary: CryoEM validation toolkit
5
5
  Home-page: https://test.pypi.org/project/va/
6
6
  Author: Zhe Wang
@@ -30,6 +30,7 @@ va/metrics/map_map_cc.py
30
30
  va/metrics/overlap_percentage.py
31
31
  va/metrics/phaserandomization.py
32
32
  va/metrics/phenix_cc.py
33
+ va/metrics/phenix_locres.py
33
34
  va/metrics/phenix_mm.py
34
35
  va/metrics/projections.py
35
36
  va/metrics/qscore.py
@@ -47,6 +48,7 @@ va/utils/ChimeraxViews.py
47
48
  va/utils/MapProcessor.py
48
49
  va/utils/Model.py
49
50
  va/utils/__init__.py
51
+ va/utils/chimerax_combined_locres.py
50
52
  va/utils/cl_weights.pth
51
53
  va/utils/log_utils.py
52
54
  va/utils/misc.py
@@ -151,13 +151,18 @@ def allruns(validationobj, runs):
151
151
  # validationobj.phrand()
152
152
 
153
153
  # Resmap local resolution
154
- if 'locres' in runs:
155
- validationobj.mask_volume()
156
- validationobj.locres_resmap()
157
- validationobj.locres_histo()
158
- validationobj.locres_residue()
159
- validationobj.model_map_ration()
160
- validationobj.locres_map()
154
+ # if 'locres' in runs:
155
+ # validationobj.mask_volume()
156
+ # validationobj.locres_resmap()
157
+ # validationobj.locres_histo()
158
+ # validationobj.locres_residue()
159
+ # validationobj.model_map_ration()
160
+ # validationobj.locres_map()
161
+
162
+ if 'locres_phe' in runs:
163
+ validationobj.phenix_local_resolution(run_phenix=True)
164
+ elif 'locres_phe_results' in runs:
165
+ validationobj.phenix_local_resolution(run_phenix=False)
161
166
 
162
167
  # EMringer
163
168
  if 'emringer' in runs:
@@ -0,0 +1,324 @@
1
+ """Run PHENIX local-resolution calculations and serialize their results."""
2
+
3
+ import json
4
+ import os
5
+ import re
6
+ import subprocess
7
+ import sys
8
+ from distutils.spawn import find_executable
9
+
10
+
11
+ EXECUTABLE = "phenix.local_resolution"
12
+
13
+
14
+ def find_local_resolution_executable():
15
+ """Find the PHENIX local-resolution executable on ``PATH``."""
16
+ return find_executable(EXECUTABLE)
17
+
18
+
19
+ def validate_inputs(half_maps):
20
+ """Return validation errors for the two required half maps."""
21
+ errors = []
22
+ if len(half_maps) != 2:
23
+ return ["PHENIX local resolution requires exactly two half maps."]
24
+ for label, path in (("half map 1", half_maps[0]), ("half map 2", half_maps[1])):
25
+ if not path or not os.path.isfile(path):
26
+ errors.append("Required {} is missing: {}".format(label, path))
27
+ return errors
28
+
29
+
30
+ def build_command(executable, half_maps, output_dir, resolution=None):
31
+ """Build a PHENIX command with both documented half-map PHIL inputs."""
32
+ command = [
33
+ executable,
34
+ "input_files.map_model.half_map={}".format(os.path.abspath(half_maps[0])),
35
+ "input_files.map_model.half_map={}".format(os.path.abspath(half_maps[1])),
36
+ "output.prefix={}".format(os.path.join(os.path.abspath(output_dir), "phenix_locres")),
37
+ "output.overwrite=True",
38
+ ]
39
+ if resolution is not None:
40
+ command.append("crystal_info.resolution={}".format(resolution))
41
+ return command
42
+
43
+
44
+ def find_output_map(output_dir, half_maps, stdout=""):
45
+ """Locate the local-resolution map reported by PHENIX, excluding full maps."""
46
+ match = re.search(r"^\s*local_resolution_map\s*:\s*(\S+)\s*$", stdout, re.MULTILINE)
47
+ if match:
48
+ reported_path = match.group(1)
49
+ candidate = reported_path if os.path.isabs(reported_path) else os.path.join(output_dir, reported_path)
50
+ if os.path.isfile(candidate):
51
+ return candidate
52
+
53
+ candidates = []
54
+ for half_map in half_maps:
55
+ half_map_stem = os.path.splitext(os.path.basename(half_map))[0]
56
+ candidate = os.path.join(output_dir, "{}_local_resolution.ccp4".format(half_map_stem))
57
+ if os.path.isfile(candidate) and candidate not in candidates:
58
+ candidates.append(candidate)
59
+ return candidates[0] if len(candidates) == 1 else None
60
+
61
+
62
+ def output_map_paths(output_dir, half_maps):
63
+ """Return the expected local-resolution and full-map output paths."""
64
+ pair_name = "{}_{}".format(
65
+ os.path.basename(half_maps[0]), os.path.basename(half_maps[1])
66
+ )
67
+ return (os.path.join(output_dir, "{}_local_resolution.map".format(pair_name)),
68
+ os.path.join(output_dir, "{}_full.map".format(pair_name)))
69
+
70
+
71
+ def rename_output_maps(output_dir, half_maps, stdout=""):
72
+ """Rename PHENIX local-resolution and sharpened maps using both input names."""
73
+ local_map = find_output_map(output_dir, half_maps, stdout)
74
+ if local_map is None:
75
+ return None, None
76
+
77
+ full_match = re.search(r"^\s*full_map\s*:\s*(\S+)\s*$", stdout, re.MULTILINE)
78
+ full_map = None
79
+ if full_match:
80
+ reported_path = full_match.group(1)
81
+ candidate = reported_path if os.path.isabs(reported_path) else os.path.join(output_dir, reported_path)
82
+ if os.path.isfile(candidate):
83
+ full_map = candidate
84
+
85
+ local_destination, full_destination = output_map_paths(output_dir, half_maps)
86
+ os.replace(local_map, local_destination)
87
+
88
+ if full_map is not None:
89
+ os.replace(full_map, full_destination)
90
+ else:
91
+ full_destination = None
92
+ return local_destination, full_destination
93
+
94
+
95
+ def read_map_statistics(map_path):
96
+ """Read basic statistics from a PHENIX local-resolution map."""
97
+ try:
98
+ import mrcfile
99
+ import numpy as np
100
+ except ImportError as exc:
101
+ raise RuntimeError("mrcfile and numpy are required to read the PHENIX map: {}".format(exc))
102
+
103
+ with mrcfile.open(map_path, permissive=False) as map_file:
104
+ values = np.asarray(map_file.data, dtype=float)
105
+ values = values[np.isfinite(values)]
106
+ if values.size == 0:
107
+ raise ValueError("Local-resolution map contains no finite values.")
108
+ return {
109
+ "min": float(values.min()),
110
+ "max": float(values.max()),
111
+ "mean": float(values.mean()),
112
+ }
113
+
114
+
115
+ def reuse_local_resolution(half_maps, output_dir):
116
+ """Load an existing PHENIX output map without invoking PHENIX.
117
+
118
+ Returns:
119
+ dict or None: A result suitable for post-processing, or None if the
120
+ expected map is missing or unreadable.
121
+ """
122
+ if len(half_maps) != 2 or not all(half_maps):
123
+ print('Cannot reuse PHENIX local resolution: both half-map names are required.')
124
+ return None
125
+ output_map, full_map = output_map_paths(output_dir, half_maps)
126
+ if not os.path.isfile(output_map):
127
+ print('No existing PHENIX local-resolution map found: {}'.format(output_map))
128
+ return None
129
+ try:
130
+ statistics = read_map_statistics(output_map)
131
+ except Exception as exc:
132
+ print('Cannot read existing PHENIX local-resolution map {}: {}'.format(output_map, exc))
133
+ return None
134
+ return {
135
+ 'status': 'success',
136
+ 'method': 'phenix',
137
+ 'output_map': os.path.relpath(output_map, output_dir),
138
+ 'full_map': os.path.relpath(full_map, output_dir) if os.path.isfile(full_map) else None,
139
+ 'statistics': statistics,
140
+ 'reused': True,
141
+ }
142
+
143
+
144
+ def run_local_resolution(half_maps, output_dir, resolution=None):
145
+ """Run PHENIX and return a structured success or error result."""
146
+ result = {"status": "error", "method": "phenix", "errors": []}
147
+ errors = validate_inputs(half_maps)
148
+ if errors:
149
+ result["errors"] = errors
150
+ return result
151
+
152
+ executable = find_local_resolution_executable()
153
+ if not executable:
154
+ result["errors"] = ["PHENIX executable {} is not available.".format(EXECUTABLE)]
155
+ return result
156
+
157
+ os.makedirs(output_dir, exist_ok=True)
158
+ command = build_command(executable, half_maps, output_dir, resolution)
159
+ print(f"Phenix local resolution command: {' '.join(command)}")
160
+ try:
161
+ process = subprocess.Popen(
162
+ command,
163
+ cwd=output_dir,
164
+ stdout=subprocess.PIPE,
165
+ stderr=subprocess.STDOUT,
166
+ text=True,
167
+ bufsize=1,
168
+ )
169
+ output_lines = []
170
+ for line in process.stdout:
171
+ print(line, end="", flush=True)
172
+ output_lines.append(line)
173
+ return_code = process.wait()
174
+ except OSError as exc:
175
+ result["errors"] = ["Could not start {}: {}".format(EXECUTABLE, exc)]
176
+ return result
177
+
178
+ result["stdout"] = "".join(output_lines)
179
+ result["return_code"] = return_code
180
+ if return_code != 0:
181
+ result["errors"] = ["{} exited with code {}.".format(EXECUTABLE, return_code)]
182
+ return result
183
+
184
+ output_map, full_map = rename_output_maps(output_dir, half_maps, result["stdout"])
185
+ if output_map is None or not os.path.isfile(output_map):
186
+ result["errors"] = ["PHENIX did not produce one identifiable local-resolution map."]
187
+ return result
188
+ try:
189
+ statistics = read_map_statistics(output_map)
190
+ except Exception as exc:
191
+ result["errors"] = ["Could not read PHENIX local-resolution map: {}".format(exc)]
192
+ return result
193
+
194
+ return {
195
+ "status": "success",
196
+ "method": "phenix",
197
+ "output_map": os.path.relpath(output_map, output_dir),
198
+ "full_map": os.path.relpath(full_map, output_dir) if full_map else None,
199
+ "statistics": statistics,
200
+ "return_code": return_code,
201
+ }
202
+
203
+
204
+ def write_result(output_path, result):
205
+ """Write a PHENIX local-resolution JSON fragment."""
206
+ with open(output_path, "w", encoding="utf-8") as output_file:
207
+ json.dump({"phenix_local_resolution": result}, output_file, indent=2)
208
+ return output_path
209
+
210
+
211
+ def create_local_resolution_views(chimerax, map_path, resolution_map, contour, output_dir, prefix):
212
+ """Generate three ChimeraX surface views colored by local resolution."""
213
+ if not chimerax:
214
+ raise RuntimeError("ChimeraX executable is not available.")
215
+ for path in (map_path, resolution_map):
216
+ if not path or not os.path.isfile(path):
217
+ raise FileNotFoundError("Map file does not exist: {}".format(path))
218
+ if contour is None:
219
+ raise ValueError("A contour level is required for the surface view.")
220
+
221
+ def quoted(path):
222
+ return '"{}"'.format(os.path.abspath(path).replace('\\', '\\\\').replace('"', '\\"'))
223
+
224
+ script_path = os.path.join(output_dir, prefix + '_chimerax.cxc')
225
+ images = {
226
+ 'original': {axis: prefix + '_' + axis.upper() + '.png' for axis in ('x', 'y', 'z')},
227
+ 'scaled': {axis: prefix + '_scaled_' + axis.upper() + '.png' for axis in ('x', 'y', 'z')},
228
+ }
229
+ with open(script_path, 'w', encoding='utf-8') as script:
230
+ script.write('open {}\nvolume #1 style surface\nvolume #1 step 1\n'.format(quoted(map_path)))
231
+ script.write('volume #1 level {}\n'.format(float(contour)))
232
+ script.write('open {}\nvolume #2 style surface\nvolume #2 step 1\n'.format(quoted(resolution_map)))
233
+ script.write('hide #2 models\nset bgColor #B8B8B8\ncolor sample #1 map #2 palette blue:white:red range full key true\n')
234
+ script.write('key pos 0.25,0.05 size 0.53,0.015 labelSide right/bottom fontSize 9 border false labelColor black\n')
235
+ for axis, rotation in (('z', ''), ('x', 'turn y 90\n'), ('y', 'turn x -90\n')):
236
+ script.write('view orient\n' + rotation)
237
+ script.write('view #1 pad 0.25 cofr true\n')
238
+ script.write('save {} width 1200 height 1200\n'.format(
239
+ quoted(os.path.join(output_dir, images['original'][axis]))))
240
+ script.write('save {} width 300 height 300\n'.format(
241
+ quoted(os.path.join(output_dir, images['scaled'][axis]))))
242
+ script.write('exit\n')
243
+ has_display = bool(os.environ.get('DISPLAY') or os.environ.get('WAYLAND_DISPLAY'))
244
+ if sys.platform == 'darwin' and not os.environ.get('SSH_CONNECTION'):
245
+ has_display = True
246
+ command = [chimerax]
247
+ if not has_display:
248
+ command.extend(['--offscreen', '--nogui'])
249
+ command.append(script_path)
250
+ subprocess.run(command, cwd=output_dir, check=True)
251
+ missing = [
252
+ name for size_images in images.values() for name in size_images.values()
253
+ if not os.path.isfile(os.path.join(output_dir, name))
254
+ ]
255
+ if missing:
256
+ raise RuntimeError('ChimeraX did not produce images: {}'.format(', '.join(missing)))
257
+ return images
258
+
259
+
260
+ def create_combined_local_resolution_views(chimerax, map_path, resolution_map, model_path,
261
+ contour, residue_data, output_dir, prefix):
262
+ """Render a transparent map and residue-colored model with a shared scale."""
263
+ if not chimerax:
264
+ raise RuntimeError('ChimeraX executable is not available.')
265
+ for path in (map_path, resolution_map, model_path):
266
+ if not path or not os.path.isfile(path):
267
+ raise FileNotFoundError('Input file does not exist: {}'.format(path))
268
+ if contour is None:
269
+ raise ValueError('An author contour level is required for the primary map.')
270
+ values = residue_data['localResolution']
271
+ if not values:
272
+ raise ValueError('No residue local-resolution values are available.')
273
+ def quoted(path):
274
+ return '"{}"'.format(os.path.abspath(path).replace('\\', '\\\\').replace('"', '\\"'))
275
+
276
+ script_path = os.path.join(output_dir, prefix + '_chimerax.cxc')
277
+ config_path = os.path.join(output_dir, prefix + '_color_config.txt')
278
+ residue_specs = []
279
+ for residue in residue_data['residue']:
280
+ match = re.fullmatch(r'([^:\s/]+):(-?\d+)\s+\S+', residue)
281
+ if not match:
282
+ raise ValueError('Unsupported residue identifier: {}'.format(residue))
283
+ residue_specs.append('/{}:{}'.format(match.group(1), match.group(2)))
284
+ if len(residue_specs) != len(values):
285
+ raise ValueError('Residue identifiers and local-resolution values do not match.')
286
+ with open(config_path, 'w', encoding='utf-8') as config_file:
287
+ json.dump({'residues': residue_specs, 'values': values}, config_file, allow_nan=False)
288
+ images = {
289
+ 'original': {axis: prefix + '_' + axis.upper() + '.png' for axis in ('x', 'y', 'z')},
290
+ 'scaled': {axis: prefix + '_scaled_' + axis.upper() + '.png' for axis in ('x', 'y', 'z')},
291
+ }
292
+ with open(script_path, 'w', encoding='utf-8') as script:
293
+ script.write('open {}\nvolume #1 style surface\nvolume #1 step 1\n'.format(quoted(map_path)))
294
+ script.write('volume #1 level {}\n'.format(float(contour)))
295
+ script.write('open {}\nhide #2 models\n'.format(quoted(resolution_map)))
296
+ rescolor_script = os.path.join(os.path.dirname(__file__), '..', 'utils', 'rescolor.py')
297
+ color_script = os.path.join(os.path.dirname(__file__), '..', 'utils', 'chimerax_combined_locres.py')
298
+ script.write('open {} id #10\nopen {}\n'.format(quoted(model_path), quoted(rescolor_script)))
299
+ script.write('show #10 ribbons\nhide #10 atoms\n')
300
+ script.write('runscript {} {}\n'.format(quoted(color_script), quoted(config_path)))
301
+ script.write('transparency #1 65 target s\n')
302
+ script.write('lighting soft\n')
303
+ script.write('set bgColor #B8B8B8\n')
304
+ for axis, rotation in (('z', ''), ('x', 'turn y 90\n'), ('y', 'turn x -90\n')):
305
+ script.write('view orient\n' + rotation + 'view #1 pad 0.25 cofr true\n')
306
+ script.write('save {} width 1200 height 1200\n'.format(
307
+ quoted(os.path.join(output_dir, images['original'][axis]))))
308
+ script.write('save {} width 300 height 300\n'.format(
309
+ quoted(os.path.join(output_dir, images['scaled'][axis]))))
310
+ script.write('exit\n')
311
+ has_display = bool(os.environ.get('DISPLAY') or os.environ.get('WAYLAND_DISPLAY'))
312
+ if sys.platform == 'darwin' and not os.environ.get('SSH_CONNECTION'):
313
+ has_display = True
314
+ command = [chimerax]
315
+ if not has_display:
316
+ command.extend(['--offscreen', '--nogui'])
317
+ subprocess.run(command + [script_path], cwd=output_dir, check=True)
318
+ missing = [
319
+ name for size_images in images.values() for name in size_images.values()
320
+ if not os.path.isfile(os.path.join(output_dir, name))
321
+ ]
322
+ if missing:
323
+ raise RuntimeError('ChimeraX did not produce images: {}'.format(', '.join(missing)))
324
+ return images
@@ -2,6 +2,90 @@ from va.utils.misc import floattohex, scale_values, scale_value, float_to_hex
2
2
  from va.utils.MapProcessor import MapProcessor
3
3
  from va.utils.ChimeraxViews import ChimeraxViews
4
4
  import numpy as np
5
+ import os
6
+ import mrcfile
7
+
8
+ LOCAL_RESOLUTION_PALETTE = [(0, 0, 255), (255, 255, 255), (255, 0, 0)]
9
+
10
+
11
+ def interpolate_atom_local_resolutions(map_path, inmodels):
12
+ """Interpolate map values at atom positions and summarize them per residue.
13
+
14
+ Args:
15
+ map_path (str): Local-resolution MRC/CCP4 map path.
16
+ inmodels: Model or list of models with Bio.PDB-style atom coordinates.
17
+
18
+ Returns:
19
+ tuple: Model-indexed atom and residue dictionaries. Outside-map and
20
+ non-finite samples have a null atom value and are excluded from means.
21
+ """
22
+ from scipy.ndimage import map_coordinates
23
+
24
+ models = inmodels if isinstance(inmodels, list) else [inmodels]
25
+ atom_results, residue_results = {}, {}
26
+ with mrcfile.open(map_path, permissive=False) as local_map:
27
+ header = local_map.header
28
+ axis_order = [int(header.mapc), int(header.mapr), int(header.maps)]
29
+ if sorted(axis_order) != [1, 2, 3]:
30
+ raise ValueError('Invalid local-resolution map axis order: {}'.format(axis_order))
31
+ voxel_sizes = local_map.voxel_size.tolist()
32
+ angles = [float(header.cellb.alpha), float(header.cellb.beta), float(header.cellb.gamma)]
33
+ transform = MapProcessor.map_matrix(voxel_sizes, angles)
34
+ origin = np.array([header.origin.x, header.origin.y, header.origin.z], dtype=float)
35
+ starts = np.array([header.nxstart, header.nystart, header.nzstart], dtype=float)
36
+ shape = np.array(local_map.data.shape)
37
+
38
+ for model_index, model in enumerate(models):
39
+ atoms, residue_names, residue_values, chain_values = [], [], [], {}
40
+ for chain in model.get_chains():
41
+ for residue in chain.get_residues():
42
+ sampled_values = []
43
+ for atom in residue.get_atoms():
44
+ element = (getattr(atom, 'element', '') or '').strip().upper()
45
+ xyz_index = transform.dot(np.asarray(atom.coord, dtype=float) - origin)
46
+ crs_index = xyz_index[np.array(axis_order) - 1] - starts
47
+ zyx_index = crs_index[::-1]
48
+ value = None
49
+ if np.all(np.isfinite(zyx_index)) and np.all(zyx_index >= 0) and np.all(zyx_index <= shape - 1):
50
+ sampled = float(map_coordinates(local_map.data, zyx_index[:, None], order=1, mode='nearest', prefilter=False)[0])
51
+ if np.isfinite(sampled):
52
+ value = sampled
53
+ sampled_values.append(value)
54
+ atoms.append({
55
+ 'chain': str(chain.id), 'residue_number': int(residue.id[1]),
56
+ 'insertion_code': str(residue.id[2]).strip(),
57
+ 'hetero_flag': str(residue.id[0]).strip(),
58
+ 'residue_name': str(residue.resname), 'atom_name': str(atom.name),
59
+ 'altloc': str(atom.get_altloc()).strip(), 'element': element,
60
+ 'serial_number': getattr(atom, 'serial_number', None),
61
+ 'local_resolution': value,
62
+ })
63
+ if sampled_values:
64
+ value = round(float(np.mean(sampled_values)), 4)
65
+ residue_names.append('{}:{} {}'.format(chain.id, residue.id[1], residue.resname))
66
+ residue_values.append(value)
67
+ chain_values.setdefault(str(chain.id), []).append(value)
68
+
69
+ model_name = os.path.basename(model.filename)
70
+ atom_results[str(model_index)] = {'name': model_name, 'atoms': atoms}
71
+ if not residue_values:
72
+ continue
73
+ lower, upper = min(residue_values), max(residue_values)
74
+ colors = [float_to_hex((value - lower) / (upper - lower) if upper > lower else 0.5,
75
+ LOCAL_RESOLUTION_PALETTE)
76
+ for value in residue_values]
77
+ chain_summary = {}
78
+ for chain, values in chain_values.items():
79
+ mean = round(float(np.mean(values)), 3)
80
+ color_value = (mean - lower) / (upper - lower) if upper > lower else 0.5
81
+ chain_summary[chain] = {'value': mean, 'color': float_to_hex(max(0, min(1, color_value)), LOCAL_RESOLUTION_PALETTE),
82
+ 'numberOfResidues': len(values)}
83
+ residue_results[str(len(residue_results))] = {
84
+ 'name': model_name,
85
+ 'data': {'residue': residue_names, 'localResolution': residue_values,
86
+ 'color': colors, 'chainResolution': chain_summary},
87
+ }
88
+ return atom_results, residue_results
5
89
 
6
90
 
7
91
  def local_resolution_json(map, inmodels, radius, value_range):
@@ -98,7 +182,8 @@ def local_resolution_json(map, inmodels, radius, value_range):
98
182
 
99
183
  data_result['residue'] = allkeys
100
184
  data_result['localResolution'] = allvalues
101
- data_result['color'] = [float_to_hex(i) for i in scale_values(allvalues, new_min, new_max)]
185
+ data_result['color'] = [float_to_hex(i, LOCAL_RESOLUTION_PALETTE)
186
+ for i in scale_values(allvalues, new_min, new_max)]
102
187
  # data_result['color'] = [float_to_hex(i) for i in scale_values(allvalues, 0, 1)]
103
188
  data_result['chainResolution'] = chainaiscore
104
189
  result[str(modelcount-1)] = {'name': modelname, 'data': data_result}
@@ -412,16 +412,19 @@ def get_masked_indices(input_mask):
412
412
  return None
413
413
 
414
414
 
415
- def localres_histogram(mapone, primary_mapname, resolution=None):
415
+ def localres_histogram(mapone, primary_mapname, resolution=None, local_resolution_path=None):
416
416
  """
417
417
  local resolution histogram
418
418
  """
419
419
 
420
420
  mapname = os.path.basename(mapone)
421
421
  output_path = os.path.dirname(mapone)
422
- file_pattern = '{}/{}_relion/*_locres.mrc'.format(output_path, primary_mapname)
423
- resmapname_glob = glob.glob(file_pattern)
424
- resmapname = resmapname_glob[0] if len(resmapname_glob) > 0 else None
422
+ if local_resolution_path is None:
423
+ file_pattern = '{}/{}_relion/*_locres.mrc'.format(output_path, primary_mapname)
424
+ resmapname_glob = glob.glob(file_pattern)
425
+ resmapname = resmapname_glob[0] if len(resmapname_glob) > 0 else None
426
+ else:
427
+ resmapname = local_resolution_path
425
428
  mask_file = f'{output_path}/{primary_mapname}_relion/mask/{primary_mapname}_mask.mrc'
426
429
  bin_mask = get_masked_indices(mask_file)
427
430
  final_dict = {}
@@ -910,6 +910,7 @@ class PreParation:
910
910
  'volume': False, 'fsc': False, 'raps': False, 'mapmodel': False, 'inclusion': False,
911
911
  'largestvariance': False, 'mask': False, 'symmetry': False, 'rmmcc': False, 'smoc': False,
912
912
  'resccc': False, 'emringer': False, 'strudel': False, '3dfsc': False, 'locres': False,
913
+ 'locres_phe': False, 'locres_phe_results': False,
913
914
  'phrand': False, 'predictcontour': False, 'mapmapcc': False}
914
915
  for key in resdict.keys():
915
916
  if key in runs:
@@ -924,8 +925,10 @@ class PreParation:
924
925
  if self.mapname is not None:
925
926
  runlist.extend(['projection', 'central', 'surface', 'volume', 'density', 'raps', 'largestvariance',
926
927
  'mask', 'fsc', 'mmfsc', 'rmmcc', 'symmetry', 'qscore', 'strudel', 'emringer', '3dfsc',
927
- 'smoc', 'resccc', 'locres', 'phrand', 'predictcontour', 'mapmapcc',
928
+ 'smoc', 'resccc', 'locres', 'locres_phe', 'phrand', 'predictcontour', 'mapmapcc',
928
929
  ])
930
+ if 'locres_phe_results' in runs:
931
+ runlist.append('locres_phe_results')
929
932
 
930
933
  if self.masks is None:
931
934
  runlist.remove('mask')
@@ -162,7 +162,8 @@ class ChimeraxViews:
162
162
  surface_file_name = '{}/{}_{}'.format(self.va_dir, model_name, map_name)
163
163
  model = f'{self.va_dir}/{model_name}'
164
164
  if data_type == 'residue_local_resolution':
165
- list_items = list(itertools.islice(min_median_max.items(), 3))
165
+ items = min_median_max.items() if isinstance(min_median_max, dict) else min_median_max
166
+ list_items = list(itertools.islice(items, 3))
166
167
  min_value, min_color = list_items[0]
167
168
  median_value, median_color = list_items[1]
168
169
  max_value, max_color = list_items[2]
@@ -187,7 +188,7 @@ class ChimeraxViews:
187
188
  # )
188
189
  if count == 0:
189
190
  count += 1
190
- fp.write(f'rescolors /{chain}:{res} {color} ')
191
+ fp.write(f'rescolors /{chain}:{res} {color}' + ('\n' if number_of_item == 1 else ' '))
191
192
  elif count == number_of_item - 1:
192
193
  fp.write(f'/{chain}:{res} {color}\n')
193
194
  else:
@@ -197,29 +198,33 @@ class ChimeraxViews:
197
198
  if data_type == 'residue_local_resolution':
198
199
  fp.write(
199
200
  'key showTool true\n'
200
- 'key size 0.25000,0.02000\n'
201
- 'key pos 0.72000,0.05000\n'
202
- f'key blue-white-red :{round(min_value, 2)} :{round((min_value+max_value)/2, 2)} :{round(max_value, 2)}\n'
203
- f'key {min_color}:{round(min_value, 1)} {median_color}:{round(median_value, 1)} {max_color}:{round(max_value, 1)}\n'
201
+ 'key size 0.50000,0.02000\n'
202
+ 'key pos 0.25000,0.05000\n'
203
+ 'key numericLabelSpacing equal\n'
204
+ 'key border false\n'
205
+ 'key labelColor black\n'
206
+ f'key blue:{min_value:.2f} white:{median_value:.2f} red:{max_value:.2f}\n'
204
207
  )
208
+ framing = 'view pad 0.25 cofr true\n' if data_type == 'residue_local_resolution' else 'view cofr True\n'
209
+ background = 'set bgColor #B8B8B8\n' if data_type == 'residue_local_resolution' else 'set bgColor white\n'
205
210
  fp.write(
206
211
  # 'key showTool true\n'
207
212
  # 'key size 0.25000,0.02000\n'
208
213
  # 'key pos 0.72000,0.05000\n'
209
214
  # f'key blue-white-red :{round(min, 2)} :{round((min+max)/2, 2)} :{round(max, 2)}\n'
210
215
  # f'key {min_color}:{round(min_value, 1)} {median_color}:{round(median_value, 1)} {max_color}:{round(max_value, 1)}\n'
211
- 'set bgColor white\n'
216
+ background +
212
217
  'lighting soft\n'
213
- 'view cofr True\n'
218
+ + framing +
214
219
  f'save {str(surface_file_name)}_z{cur_type}.jpeg supersample 1 width 1200 height 1200\n'
215
220
  'turn x -90\n'
216
221
  'turn y -90\n'
217
- 'view cofr True\n'
222
+ + framing +
218
223
  f'save {str(surface_file_name)}_y{cur_type}.jpeg supersample 1 width 1200 height 1200\n'
219
224
  'view orient\n'
220
225
  'turn x 90\n'
221
226
  'turn z 90\n'
222
- 'view cofr True\n'
227
+ + framing +
223
228
  f'save {str(surface_file_name)}_x{cur_type}.jpeg supersample 1 width 1200 height 1200\n'
224
229
  'close all\n'
225
230
  'exit'
@@ -421,22 +426,17 @@ class ChimeraxViews:
421
426
  model_name = cur_model[key]
422
427
  min_middle_max = None
423
428
  if data_type == 'residue_local_resolution':
424
- min_middle_max = {}
429
+ min_middle_max = []
425
430
  min_value = min(values)
426
431
  max_value = max(values)
427
- real_middle = (min_value + max_value) / 2
428
- difference = [abs(value - real_middle) for value in values]
429
- middle_index = difference.index(min(difference))
430
- middle_value = values[middle_index]
431
- min_value, middle_value, max_value = (min(values), middle_value, max(values))
432
+ middle_value = (min_value + max_value) / 2
432
433
  min_index = values.index(min_value)
433
434
  max_index = values.index(max_value)
434
435
  min_colour = colors[min_index]
435
- middle_colour = colors[middle_index]
436
+ middle_colour = '#FFFFFF'
436
437
  max_colour = colors[max_index]
437
- min_middle_max[min_value] = min_colour
438
- min_middle_max[middle_value] = middle_colour
439
- min_middle_max[max_value] = max_colour
438
+ min_middle_max.extend([(min_value, min_colour), (middle_value, middle_colour),
439
+ (max_value, max_colour)])
440
440
  # result = self.json_to_attributes(data_type)
441
441
  chimerax_file_name = self.write_residue_cxc(colors, residues, map_name, model_name, data_type, min_middle_max)
442
442
  # Use attributes file for colouring
@@ -0,0 +1,80 @@
1
+ """Color a combined map/model view using its surface and residue values.
2
+
3
+ This script is run by ChimeraX with a configuration filename as its argument.
4
+ """
5
+
6
+ import json
7
+ import sys
8
+
9
+ import numpy as np
10
+ from chimerax.core.commands import run
11
+ from chimerax.core.errors import UserError
12
+
13
+
14
+ def color_combined_view(session, config):
15
+ """Sample the contoured map surface and apply one shared color range."""
16
+ primary = session.models.list(model_id=(1,))
17
+ resolution = session.models.list(model_id=(2,))
18
+ if len(primary) != 1 or len(resolution) != 1:
19
+ raise UserError('Expected primary and local-resolution maps #1 and #2.')
20
+ primary_map, resolution_map = primary[0], resolution[0]
21
+ primary_map.update_drawings()
22
+ all_values = list(config['values'])
23
+ sampled_count = 0
24
+ map_lower, map_upper = None, None
25
+ for surface in primary_map.surfaces:
26
+ if not surface.display or surface.vertices is None or not len(surface.vertices):
27
+ continue
28
+ values, outside = resolution_map.interpolated_values(
29
+ surface.vertices, surface.scene_position, out_of_bounds_list=True
30
+ )
31
+ inside = np.ones(len(values), dtype=bool)
32
+ inside[outside] = False
33
+ sampled = values[inside & np.isfinite(values)]
34
+ if len(sampled):
35
+ sample_lower, sample_upper = float(sampled.min()), float(sampled.max())
36
+ map_lower = sample_lower if map_lower is None else min(map_lower, sample_lower)
37
+ map_upper = sample_upper if map_upper is None else max(map_upper, sample_upper)
38
+ sampled_count += len(sampled)
39
+ if not sampled_count:
40
+ raise UserError('No valid local-resolution values on the displayed map surface.')
41
+ model_lower, model_upper = min(all_values), max(all_values)
42
+ lower, upper = min(map_lower, model_lower), max(map_upper, model_upper)
43
+ if lower == upper:
44
+ lower, upper = lower - 0.01, upper + 0.01
45
+
46
+ run(session, 'color sample #1 map #2 palette blue:white:red range {},{} key true'.format(lower, upper))
47
+ run(session, 'key blue: white: red: pos 0.25,0.08 size 0.5,0.02 border false')
48
+ for label, limits, text_y, arrow_start, arrow_end in (
49
+ ('Map', (map_lower, map_upper), 0.073, 0.074, 0.08),
50
+ ('Model', (model_lower, model_upper), 0.110, 0.110, 0.10),
51
+ ):
52
+ run(session, '2dlabels text "{}" color black size 9 xpos 0.16 ypos {}'.format(label, text_y))
53
+ values = (limits[0], (limits[0] + limits[1]) / 2, limits[1])
54
+ for value in sorted(set(values)):
55
+ xpos = 0.25 + 0.5 * (value - lower) / (upper - lower)
56
+ run(session, '2dlabels arrow start {},{} end {},{} color black weight 0.3 headStyle pointer'.format(
57
+ xpos, arrow_start, xpos, arrow_end))
58
+ label_x = xpos
59
+ if label == 'Map' and value == map_lower:
60
+ label_x = 0.25 + 0.5 * (model_lower - lower) / (upper - lower)
61
+ run(session, '2dlabels text "{:.2f}" color black size 9 xpos {} ypos {}'.format(
62
+ value, max(0.0, label_x - 0.012), text_y))
63
+ colors = []
64
+ for spec, value in zip(config['residues'], config['values']):
65
+ position = (value - lower) / (upper - lower)
66
+ palette = ((0, 0, 255), (255, 255, 255), (255, 0, 0))
67
+ index = min(int(position * 2), 1)
68
+ fraction = position * 2 - index
69
+ rgb = tuple(int(start + fraction * (end - start))
70
+ for start, end in zip(palette[index], palette[index + 1]))
71
+ colors.append('{} #{:02x}{:02x}{:02x}'.format(spec, *rgb))
72
+ run(session, 'rescolors ' + ' '.join(colors))
73
+ session.logger.info('Combined local-resolution ranges: map {:.4f}-{:.4f} Å, model {:.4f}-{:.4f} Å '
74
+ '(shared {:.4f}-{:.4f} Å; {} surface samples).'.format(
75
+ map_lower, map_upper, model_lower, model_upper, lower, upper, sampled_count))
76
+
77
+
78
+ if 'session' in globals():
79
+ with open(sys.argv[1], encoding='utf-8') as config_file:
80
+ color_combined_view(session, json.load(config_file))
@@ -62,6 +62,9 @@ from va.metrics.threedfsc import run_threedfsc
62
62
  from va.metrics.smoc import run_smoc
63
63
  from va.metrics.phenix_cc import *
64
64
  from va.metrics.phenix_mm import *
65
+ from va.metrics.phenix_locres import (run_local_resolution, reuse_local_resolution, create_local_resolution_views,
66
+ create_combined_local_resolution_views,
67
+ write_result as write_phenix_locres_result)
65
68
  from va.metrics.resmap import *
66
69
  from va.metrics.bars import *
67
70
  from va.metrics.phaserandomization import *
@@ -7269,6 +7272,100 @@ class ValidationAnalysis:
7269
7272
  print('Local resolution views time: %s' % (end - start))
7270
7273
  print('------------------------------------')
7271
7274
 
7275
+ def phenix_local_resolution(self, run_phenix=True):
7276
+ """Run PHENIX or reuse its existing map and generate result artifacts."""
7277
+ output_dir = self.workdir
7278
+ output_json = os.path.join(self.workdir, '{}_phenix_local_resolution.json'.format(self.mapname))
7279
+ half_maps = [
7280
+ getattr(self.hmodd, 'fullname', None),
7281
+ getattr(self.hmeven, 'fullname', None),
7282
+ ]
7283
+ if run_phenix:
7284
+ result = run_local_resolution(half_maps, output_dir, self.resolution)
7285
+ else:
7286
+ result = reuse_local_resolution(half_maps, output_dir)
7287
+ if result is None:
7288
+ return None
7289
+ if result.get('status') == 'success':
7290
+ resolution_map = os.path.join(output_dir, result['output_map'])
7291
+ if self.models:
7292
+ try:
7293
+ atom_data, residue_data = interpolate_atom_local_resolutions(resolution_map, self.models)
7294
+ atom_json = os.path.join(output_dir, self.mapname + '_atom_local_resolution.json')
7295
+ with open(atom_json, 'w', encoding='utf-8') as output_file:
7296
+ json.dump({'atom_local_resolution': atom_data}, output_file, indent=2, allow_nan=False)
7297
+ if residue_data:
7298
+ residue_json = os.path.join(output_dir, self.mapname + '_residue_local_resolution.json')
7299
+ with open(residue_json, 'w', encoding='utf-8') as output_file:
7300
+ json.dump({'residue_local_resolution': residue_data}, output_file, indent=2, allow_nan=False)
7301
+ _, model_chimerax = self.surface_envcheck()
7302
+ if model_chimerax:
7303
+ model_local_resolution_views(residue_json, self.mapname, 'residue_local_resolution', model_chimerax)
7304
+ combined_views = {}
7305
+ for model_result in residue_data.values():
7306
+ model_name = model_result['name']
7307
+ try:
7308
+ prefix = '{}_{}_phenix_local_resolution_combined'.format(self.mapname, model_name)
7309
+ combined_views[model_name] = create_combined_local_resolution_views(
7310
+ model_chimerax, self.map.fullname, resolution_map,
7311
+ os.path.join(output_dir, model_name), self.cl,
7312
+ model_result['data'], output_dir, prefix)
7313
+ except Exception as exc:
7314
+ sys.stderr.write('Combined local-resolution views for {} failed: {}.\n'.format(model_name, exc))
7315
+ if combined_views:
7316
+ combined_json = os.path.join(output_dir, self.mapname + '_modelmap_localresolutionviews.json')
7317
+ with open(combined_json, 'w', encoding='utf-8') as output_file:
7318
+ json.dump({'model_map_local_resolution_views': combined_views}, output_file, indent=2)
7319
+ else:
7320
+ sys.stderr.write('ChimeraX unavailable; skipping residue local-resolution views.\n')
7321
+ else:
7322
+ sys.stderr.write('No model residues had valid local-resolution samples.\n')
7323
+ except Exception as exc:
7324
+ sys.stderr.write('PHENIX atom/residue local resolution failed: {}.\n'.format(exc))
7325
+ try:
7326
+ resolution = float(self.resolution) if self.resolution is not None else None
7327
+ except (TypeError, ValueError):
7328
+ resolution = None
7329
+ try:
7330
+ histogram = localres_histogram(
7331
+ half_maps[0], self.mapname, resolution, local_resolution_path=resolution_map
7332
+ )
7333
+ if histogram:
7334
+ histogram_json = os.path.join(output_dir, self.mapname + '_localres_histo.json')
7335
+ with open(histogram_json, 'w', encoding='utf-8') as output_file:
7336
+ json.dump({'local_res_histogram': histogram}, output_file)
7337
+ except Exception as exc:
7338
+ sys.stderr.write('PHENIX local-resolution histogram failed: {}.\n'.format(exc))
7339
+ _, chimerax = self.surface_envcheck()
7340
+ views = {}
7341
+ for label, map_obj in (('primary', self.map), ('raw', self.rawmap)):
7342
+ if map_obj is None:
7343
+ print('Skipping PHENIX local-resolution {} views: map is missing.'.format(label))
7344
+ continue
7345
+ try:
7346
+ contour = self.cl if label == 'primary' else calc_level_dev(map_obj.data)[0]
7347
+ map_basename = os.path.basename(map_obj.fullname)
7348
+ prefix = '{}_phenix_local_resolution'.format(map_basename)
7349
+ views[label] = create_local_resolution_views(
7350
+ chimerax, map_obj.fullname, resolution_map, contour, output_dir, prefix
7351
+ )
7352
+ view_key = 'local_resolution_views' if label == 'primary' else 'rawmap_local_resolution_views'
7353
+ view_json = os.path.join(output_dir, '{}_localresolutionviews.json'.format(map_basename))
7354
+ with open(view_json, 'w', encoding='utf-8') as output_file:
7355
+ json.dump({view_key: views[label]}, output_file, indent=2)
7356
+ except Exception as exc:
7357
+ sys.stderr.write('PHENIX local-resolution {} views failed: {}.\n'.format(label, exc))
7358
+ if views:
7359
+ result['views'] = views
7360
+ try:
7361
+ write_phenix_locres_result(output_json, result)
7362
+ except Exception as exc:
7363
+ sys.stderr.write('Saving PHENIX local-resolution JSON failed: {}.\n'.format(exc))
7364
+ if result.get('status') != 'success':
7365
+ for error in result.get('errors', []):
7366
+ sys.stderr.write('PHENIX local-resolution error: {}\n'.format(error))
7367
+ return result
7368
+
7272
7369
  @profile_peak_memory()
7273
7370
  def emringer(self):
7274
7371
  """
@@ -18,5 +18,5 @@ under the License.
18
18
 
19
19
  """
20
20
 
21
- __version__ = '0.0.1.dev152'
21
+ __version__ = '0.0.1.dev155'
22
22
  __em_statistics_version__ = '202505.v01'
File without changes
File without changes
File without changes
File without changes
File without changes