emdbva 0.0.1.dev144__tar.gz → 0.0.1.dev146__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 (55) hide show
  1. {emdbva-0.0.1.dev144/emdbva.egg-info → emdbva-0.0.1.dev146}/PKG-INFO +1 -1
  2. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146/emdbva.egg-info}/PKG-INFO +1 -1
  3. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/emdbva.egg-info/SOURCES.txt +2 -0
  4. emdbva-0.0.1.dev146/va/audit/__init__.py +13 -0
  5. emdbva-0.0.1.dev146/va/audit/cif_revision.py +324 -0
  6. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/mainva.py +13 -0
  7. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/resmap.py +7 -7
  8. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/prepareandrun_codon_airflow.py +78 -84
  9. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/schemas/emdb_entry_full.schema.json +160 -0
  10. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/utils/misc.py +63 -37
  11. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/utils/stars.py +286 -93
  12. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/validationanalysis.py +24 -13
  13. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/version.py +1 -1
  14. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/LICENSE +0 -0
  15. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/MANIFEST.in +0 -0
  16. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/README.rst +0 -0
  17. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/emdbva.egg-info/dependency_links.txt +0 -0
  18. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/emdbva.egg-info/entry_points.txt +0 -0
  19. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/emdbva.egg-info/requires.txt +0 -0
  20. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/emdbva.egg-info/top_level.txt +0 -0
  21. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/setup.cfg +0 -0
  22. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/setup.py +0 -0
  23. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/PATHS_codon_airflow.py +0 -0
  24. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/__init__.py +0 -0
  25. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/__init__.py +0 -0
  26. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/bars.py +0 -0
  27. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/connected_percentage.py +0 -0
  28. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/contour_level_predicator.py +0 -0
  29. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/emda_mmcc.py +0 -0
  30. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/emringer.py +0 -0
  31. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/inclusion.py +0 -0
  32. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/map_data_validation.py +0 -0
  33. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/overlap_percentage.py +0 -0
  34. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/phaserandomization.py +0 -0
  35. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/phenix_cc.py +0 -0
  36. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/phenix_mm.py +0 -0
  37. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/projections.py +0 -0
  38. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/qscore.py +0 -0
  39. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/residue_locres.py +0 -0
  40. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/smoc.py +0 -0
  41. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/strudel.py +0 -0
  42. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/surfaces.py +0 -0
  43. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/threedfsc.py +0 -0
  44. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/preparation.py +0 -0
  45. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/qscores.csv +0 -0
  46. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/schemas/__init__.py +0 -0
  47. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/schemas/loader.py +0 -0
  48. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/utils/Checker.py +0 -0
  49. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/utils/ChimeraxViews.py +0 -0
  50. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/utils/MapProcessor.py +0 -0
  51. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/utils/Model.py +0 -0
  52. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/utils/__init__.py +0 -0
  53. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/utils/cl_weights.pth +0 -0
  54. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/utils/log_utils.py +0 -0
  55. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/utils/rescolor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: emdbva
3
- Version: 0.0.1.dev144
3
+ Version: 0.0.1.dev146
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.dev144
3
+ Version: 0.0.1.dev146
4
4
  Summary: CryoEM validation toolkit
5
5
  Home-page: https://test.pypi.org/project/va/
6
6
  Author: Zhe Wang
@@ -16,6 +16,8 @@ va/prepareandrun_codon_airflow.py
16
16
  va/qscores.csv
17
17
  va/validationanalysis.py
18
18
  va/version.py
19
+ va/audit/__init__.py
20
+ va/audit/cif_revision.py
19
21
  va/metrics/__init__.py
20
22
  va/metrics/bars.py
21
23
  va/metrics/connected_percentage.py
@@ -0,0 +1,13 @@
1
+ """Audit helpers used to make conservative VA execution decisions."""
2
+
3
+ from va.audit.cif_revision import (
4
+ audit_model_cifs_for_preparation,
5
+ should_run_va_for_cif,
6
+ write_cif_revision_audit_report,
7
+ )
8
+
9
+ __all__ = [
10
+ 'audit_model_cifs_for_preparation',
11
+ 'should_run_va_for_cif',
12
+ 'write_cif_revision_audit_report',
13
+ ]
@@ -0,0 +1,324 @@
1
+ """Gemmi-based audit revision checks for model CIF files."""
2
+
3
+ import os
4
+ import re
5
+ import json
6
+
7
+ import gemmi
8
+
9
+
10
+ _CATEGORY = '_pdbx_audit_revision_details'
11
+ _FIELDS = (
12
+ 'ordinal',
13
+ 'revision_ordinal',
14
+ 'data_content_type',
15
+ 'provider',
16
+ 'type',
17
+ 'description',
18
+ 'details',
19
+ )
20
+
21
+ DEFAULT_SKIP_RULES = [
22
+ {
23
+ 'type': 'Remediation',
24
+ 'description': 'Metalloprotein remediation',
25
+ 'decision': 'skip',
26
+ 'reason_code': 'metalloprotein_remediation',
27
+ },
28
+ {
29
+ 'type': 'Remediation',
30
+ 'description': 'Carbohydrate remediation',
31
+ 'decision': 'skip',
32
+ 'reason_code': 'carbohydrate_remediation',
33
+ },
34
+ ]
35
+
36
+
37
+ def _normalise_value(value):
38
+ """Normalize one Gemmi CIF value for display and comparison."""
39
+ if value is None:
40
+ return None
41
+ value = str(value).strip()
42
+ if len(value) >= 2 and value[0] == value[-1] and value[0] in "'\"":
43
+ value = value[1:-1].strip()
44
+ value = re.sub(r'\s+', ' ', value)
45
+ if value in ('?', '.') or not value:
46
+ return None
47
+ return value
48
+
49
+
50
+ def _normalise_for_match(value):
51
+ """Return a normalized case-insensitive comparison value."""
52
+ value = _normalise_value(value)
53
+ return value.casefold() if value is not None else None
54
+
55
+
56
+ def _first_block(document):
57
+ """Return the first data block across supported Gemmi versions."""
58
+ try:
59
+ return document[0]
60
+ except (TypeError, IndexError):
61
+ return document.sole_block()
62
+
63
+
64
+ def _loop_from_block(block):
65
+ """Return the audit loop or ``None`` when the category is not looped."""
66
+ for field in _FIELDS:
67
+ try:
68
+ column = block.find_loop(f'{_CATEGORY}.{field}')
69
+ except Exception:
70
+ column = None
71
+ if column is not None:
72
+ return column.get_loop() if hasattr(column, 'get_loop') else column
73
+ return None
74
+
75
+
76
+ def _rows_from_loop(loop):
77
+ """Convert a Gemmi loop into normalized row dictionaries."""
78
+ tags = [str(tag) for tag in getattr(loop, 'tags', [])]
79
+ values = list(getattr(loop, 'values', []))
80
+ if not tags:
81
+ return []
82
+
83
+ width = len(tags)
84
+ rows = []
85
+ for offset in range(0, len(values), width):
86
+ row = {}
87
+ for field in _FIELDS:
88
+ tag = f'{_CATEGORY}.{field}'
89
+ try:
90
+ index = tags.index(tag)
91
+ except ValueError:
92
+ row[field] = None
93
+ continue
94
+ value_index = offset + index
95
+ row[field] = _normalise_value(values[value_index]) if value_index < len(values) else None
96
+ rows.append(row)
97
+ return rows
98
+
99
+
100
+ def _rows_from_scalars(block):
101
+ """Read a single audit row when the category is not represented as a loop."""
102
+ row = {}
103
+ found = False
104
+ for field in _FIELDS:
105
+ try:
106
+ value = block.find_value(f'{_CATEGORY}.{field}')
107
+ except Exception:
108
+ value = None
109
+ row[field] = _normalise_value(value)
110
+ found = found or row[field] is not None
111
+ return [row] if found else []
112
+
113
+
114
+ def _issue(code, message, path):
115
+ """Build a non-fatal audit issue."""
116
+ return {
117
+ 'code': code,
118
+ 'severity': 'warning',
119
+ 'path': path,
120
+ 'message': message,
121
+ }
122
+
123
+
124
+ def _rule_matches(row, rule):
125
+ """Return whether a rule exactly matches the relevant row fields."""
126
+ return (
127
+ _normalise_for_match(row.get('type')) == _normalise_for_match(rule.get('type'))
128
+ and _normalise_for_match(row.get('description')) == _normalise_for_match(rule.get('description'))
129
+ )
130
+
131
+
132
+ def _base_decision(rows, issues):
133
+ """Return the common report structure for parsed audit rows."""
134
+ return {
135
+ 'run_validation': True,
136
+ 'decision': 'run',
137
+ 'reason_code': 'unmatched_revision',
138
+ 'message': 'No known VA-skip audit rule matched; VA will run.',
139
+ 'matched_rules': [],
140
+ 'issues': issues,
141
+ 'audit_revision_details': {
142
+ 'row_count': len(rows),
143
+ 'rows': rows,
144
+ },
145
+ }
146
+
147
+
148
+ def should_run_va_for_cif(cif_path, rules=None):
149
+ """Return a conservative VA execution decision for one model CIF.
150
+
151
+ Args:
152
+ cif_path (str): Path to a model CIF/mmCIF file.
153
+ rules (list or None): Optional replacement list of skip-rule mappings.
154
+
155
+ Returns:
156
+ dict: A stable run/skip decision with parsed audit rows and warnings.
157
+ """
158
+ if not cif_path or not os.path.isfile(cif_path):
159
+ return {
160
+ 'run_validation': True,
161
+ 'decision': 'run',
162
+ 'reason_code': 'missing_cif',
163
+ 'message': 'Model CIF file is missing; VA will run without audit gating.',
164
+ 'matched_rules': [],
165
+ 'issues': [_issue('missing_cif', f'CIF file does not exist: {cif_path}', str(cif_path))],
166
+ 'audit_revision_details': {'row_count': 0, 'rows': []},
167
+ }
168
+
169
+ try:
170
+ document = gemmi.cif.read_file(cif_path)
171
+ block = _first_block(document)
172
+ loop = _loop_from_block(block)
173
+ rows = _rows_from_loop(loop) if loop is not None else _rows_from_scalars(block)
174
+ except Exception as exc:
175
+ return {
176
+ 'run_validation': True,
177
+ 'decision': 'run',
178
+ 'reason_code': 'parse_error',
179
+ 'message': 'CIF audit data could not be parsed; VA will run.',
180
+ 'matched_rules': [],
181
+ 'issues': [_issue('parse_error', str(exc), str(cif_path))],
182
+ 'audit_revision_details': {'row_count': 0, 'rows': []},
183
+ }
184
+
185
+ if not rows:
186
+ return {
187
+ 'run_validation': True,
188
+ 'decision': 'run',
189
+ 'reason_code': 'missing_audit_category',
190
+ 'message': 'No audit revision details were found; VA will run.',
191
+ 'matched_rules': [],
192
+ 'issues': [_issue(
193
+ 'missing_audit_category',
194
+ f'{_CATEGORY} is absent or has no rows.',
195
+ _CATEGORY,
196
+ )],
197
+ 'audit_revision_details': {'row_count': 0, 'rows': []},
198
+ }
199
+
200
+ report = _base_decision(rows, [])
201
+ active_rules = DEFAULT_SKIP_RULES if rules is None else rules
202
+ matched_rules = []
203
+ matched_row_indexes = set()
204
+ for index, row in enumerate(rows):
205
+ for rule in active_rules:
206
+ if _rule_matches(row, rule):
207
+ matched_rules.append(dict(rule))
208
+ matched_row_indexes.add(index)
209
+
210
+ initial_type = _normalise_for_match('Initial release')
211
+ unrelated_rows = [
212
+ row for index, row in enumerate(rows)
213
+ if index not in matched_row_indexes
214
+ and _normalise_for_match(row.get('type')) not in (None, initial_type)
215
+ ]
216
+
217
+ if matched_rules and not unrelated_rows:
218
+ report.update({
219
+ 'run_validation': False,
220
+ 'decision': 'skip',
221
+ 'reason_code': 'known_remediation_skip',
222
+ 'message': 'A known remediation-only audit rule matched; VA validation will be skipped.',
223
+ 'matched_rules': matched_rules,
224
+ })
225
+ return report
226
+
227
+ if len(rows) == 1 and _normalise_for_match(rows[0].get('type')) == initial_type:
228
+ report.update({
229
+ 'reason_code': 'initial_release_only',
230
+ 'message': 'Only an initial release audit revision was found; VA will run.',
231
+ })
232
+ return report
233
+
234
+
235
+ def _model_cif_paths(prepobj):
236
+ """Resolve model CIF paths using the locations used by ``read_model``."""
237
+ model_names = getattr(prepobj, 'model', None) or []
238
+ if isinstance(model_names, str):
239
+ model_names = [model_names]
240
+
241
+ paths = []
242
+ for model_name in model_names:
243
+ if not model_name:
244
+ continue
245
+ if os.path.isabs(model_name):
246
+ paths.append(model_name)
247
+ continue
248
+ working_path = os.path.join(prepobj.vadir, model_name)
249
+ paths.append(working_path if os.path.isfile(working_path) else model_name)
250
+ return paths
251
+
252
+
253
+ def _aggregate_model_reports(model_paths, model_reports):
254
+ """Aggregate per-model decisions using the conservative all-skip policy."""
255
+ all_skip = bool(model_reports) and all(
256
+ report.get('decision') == 'skip' for report in model_reports
257
+ )
258
+ issues = [issue for report in model_reports for issue in report.get('issues', [])]
259
+ matched_rules = [rule for report in model_reports for rule in report.get('matched_rules', [])]
260
+ rows = [
261
+ row for report in model_reports
262
+ for row in report.get('audit_revision_details', {}).get('rows', [])
263
+ ]
264
+ return {
265
+ 'run_validation': not all_skip,
266
+ 'decision': 'skip' if all_skip else 'run',
267
+ 'reason_code': 'known_remediation_skip' if all_skip else 'model_audit_reviewed',
268
+ 'message': (
269
+ 'All model CIFs matched known remediation rules; VA validation will be skipped.'
270
+ if all_skip else 'At least one model CIF did not clearly match a skip rule; VA will run.'
271
+ ),
272
+ 'matched_rules': matched_rules,
273
+ 'issues': issues,
274
+ 'audit_revision_details': {'row_count': len(rows), 'rows': rows},
275
+ 'model_count': len(model_paths),
276
+ 'model_reports': model_reports,
277
+ }
278
+
279
+
280
+ def audit_model_cifs_for_preparation(prepobj):
281
+ """Audit all configured model CIFs for a prepared VA entry.
282
+
283
+ Args:
284
+ prepobj: Prepared VA input object with ``model`` and ``vadir`` attributes.
285
+
286
+ Returns:
287
+ dict: Aggregate audit decision and per-model reports. Missing or
288
+ malformed CIF data produces a run decision with warnings.
289
+ """
290
+ model_paths = _model_cif_paths(prepobj)
291
+ if not model_paths:
292
+ return {
293
+ 'run_validation': True,
294
+ 'decision': 'run',
295
+ 'reason_code': 'no_model',
296
+ 'message': 'No model was supplied; CIF audit gating was not applied.',
297
+ 'matched_rules': [],
298
+ 'issues': [],
299
+ 'audit_revision_details': {'row_count': 0, 'rows': []},
300
+ 'model_count': 0,
301
+ 'model_reports': [],
302
+ }
303
+
304
+ model_reports = [should_run_va_for_cif(path) for path in model_paths]
305
+ return _aggregate_model_reports(model_paths, model_reports)
306
+
307
+
308
+ def write_cif_revision_audit_report(prepobj, report):
309
+ """Write the audit fragment consumed by the existing JSON merger.
310
+
311
+ Args:
312
+ prepobj: Prepared VA input object with ``vadir`` and ``mapname``.
313
+ report (dict): Aggregate CIF audit decision.
314
+
315
+ Returns:
316
+ str: Path to the written audit report fragment.
317
+ """
318
+ report_path = os.path.join(
319
+ prepobj.vadir,
320
+ '{}_cif_revision_audit.json'.format(os.path.basename(prepobj.mapname)),
321
+ )
322
+ with open(report_path, 'w', encoding='utf-8') as output:
323
+ json.dump({'audit_validation': {'cif_revision_audit': report}}, output, indent=2)
324
+ return report_path
@@ -40,6 +40,10 @@ from va.validationanalysis import ValidationAnalysis
40
40
  from va.preparation import PreParation as prep
41
41
  from va.metrics.strudel import run_strudel
42
42
  from va.utils.log_utils import LogRedirector
43
+ from va.audit.cif_revision import (
44
+ audit_model_cifs_for_preparation,
45
+ write_cif_revision_audit_report,
46
+ )
43
47
  import threading
44
48
  try:
45
49
  from PATHS import MAP_SERVER_PATH
@@ -335,6 +339,7 @@ def reconfigure_logging(temp_logger, buffer, log_file):
335
339
 
336
340
  temp_logger.info(f"Logging reconfigured to use file: {log_file}")
337
341
 
342
+
338
343
  # @profile
339
344
  def main():
340
345
  """Run the main VA command-line entry point.
@@ -348,6 +353,14 @@ def main():
348
353
  # Preparation
349
354
  # temp_logger, buffer = setup_temporary_logging()
350
355
  prepobj = prep()
356
+
357
+ if not prepobj.args.positions:
358
+ audit_report = audit_model_cifs_for_preparation(prepobj)
359
+ write_cif_revision_audit_report(prepobj, audit_report)
360
+ if not audit_report['run_validation']:
361
+ print(audit_report['message'])
362
+ return None
363
+
351
364
  # path_to_logs = f'{prepobj.vadir}/{prepobj.mapname}_va.log'
352
365
  # reconfigure_logging(temp_logger, buffer, path_to_logs)
353
366
  position = prepobj.args.positions
@@ -180,7 +180,7 @@ def resmap_filecheck(mapone, output_path):
180
180
  return check if check else False
181
181
 
182
182
 
183
- def resmap_chimerax(mapone, output_path):
183
+ def relion_local_res_chimerax(orgmap, mapone, output_path):
184
184
  """
185
185
  Generate chimerax cmd for ResMap results
186
186
 
@@ -188,10 +188,10 @@ def resmap_chimerax(mapone, output_path):
188
188
  None. ChimeraX command files may be written to the output directory.
189
189
  """
190
190
 
191
- mapname = os.path.basename(mapone)
192
191
  output_chimerax_file = '{}{}_chimerax.cxc'.format(output_path, os.path.basename(mapone))
193
- orgmap = '{}{}_ori.map'.format(output_path, os.path.splitext(mapname)[0])
194
- resmap = '{}{}_ori_resmap.map'.format(output_path, os.path.splitext(mapname)[0])
192
+ orgmap = orgmap
193
+ # resmap = '{}{}_ori_resmap.map'.format(output_path, os.path.splitext(mapname)[0])
194
+ resmap = mapone
195
195
  header = mrcfile.open(mapone, mode='r', header_only=True)
196
196
  voxsizes = header.voxel_size.tolist()
197
197
  if all(element == voxsizes[0] for element in voxsizes):
@@ -305,12 +305,12 @@ def run_resmap_chimerax(bindisplay, locCHIMERA, cxcfile):
305
305
  if not bindisplay:
306
306
  subprocess.check_call(locCHIMERA + " --offscreen --nogui " + cxcfile, cwd=os.path.dirname(cxcfile),
307
307
  shell=True)
308
- print('Animated PNG for ResMap result has been produced.')
308
+ print('Animated PNG for local resolution result has been produced.')
309
309
  else:
310
310
  subprocess.check_call(locCHIMERA + " " + cxcfile, cwd=os.path.dirname(cxcfile), shell=True)
311
- print('Animated PNG for ResMap result has been produced.')
311
+ print('Animated PNG for local resolution result has been produced.')
312
312
  except subprocess.CalledProcessError as suberr:
313
- err = 'Saving ResMap local resolution animated png error: {}.'.format(suberr)
313
+ err = 'Saving local resolution animated png error: {}.'.format(suberr)
314
314
  errlist.append(err)
315
315
  sys.stderr.write(err + '\n')
316
316
 
@@ -627,7 +627,14 @@ class prepareandrun():
627
627
  logging.info('Validation command completed.')
628
628
  self.data_sync()
629
629
  else:
630
- logging.info('Validation command failed; skipping data sync.')
630
+ logging.error(
631
+ 'Validation command failed with exit code %s; skipping data sync. '
632
+ 'See stdout: %s and stderr: %s',
633
+ return_code,
634
+ stdout_path,
635
+ stderr_path,
636
+ )
637
+ raise subprocess.CalledProcessError(return_code, orgcmd)
631
638
 
632
639
  # predmem = self.memmsg(self.mapsize)
633
640
  # bsub -M predmem .....
@@ -665,7 +672,7 @@ class prepareandrun():
665
672
 
666
673
  def data_sync(self):
667
674
  """
668
- sync data at corresponding folder to staging and production folders
675
+ sync data at corresponding folder to public production folders
669
676
 
670
677
  :return: None
671
678
  """
@@ -673,94 +680,82 @@ class prepareandrun():
673
680
  result_folder = '{}{}/va/'.format(MAP_SERVER_PATH, sub_dir)
674
681
  id = list(self.idsubdirs.keys())[0]
675
682
  prod_folder = '{}/{}/va'.format(VA_PROD_PATH, sub_dir)
683
+ fallback_prod_folder = prod_folder.replace('/nfs/public/', '/nfs/ebi/public/', 1)
676
684
  logging.info(prod_folder)
685
+ logging.info(fallback_prod_folder)
677
686
  alljsonfile = glob.glob('{}emd_{}_all.json'.format(result_folder, id))
678
- if alljsonfile:
687
+ if not alljsonfile:
688
+ logging.info('No all json output, job failed to the end')
689
+ return None
690
+
691
+ rsync_filters = (
692
+ "--include '*/' "
693
+ "--include '*_rawmap.map' "
694
+ "--include '*_mask.mrc' "
695
+ "--exclude '*_locres.mrc' "
696
+ "--exclude '*.map' "
697
+ "--exclude '*.mrc' "
698
+ "--exclude '*.gz' "
699
+ "--exclude 'emd-*.cif' "
700
+ "--include '*' "
701
+ )
702
+
703
+ def sync_command(target_folder):
679
704
  quoted_result = shlex.quote(result_folder)
680
- quoted_prod = shlex.quote(prod_folder)
681
-
682
- rsync_filters = (
683
- "--include '*/' "
684
- "--include '*_rawmap.map' "
685
- "--include '*_mask.mrc' "
686
- "--exclude '*_locres.mrc' "
687
- "--exclude '*.map' "
688
- "--exclude '*.mrc' "
689
- "--exclude '*.gz' "
690
- "--exclude 'emd-*.cif' "
691
- "--include '*' "
705
+ quoted_target = shlex.quote(target_folder.rstrip('/'))
706
+ return (
707
+ 'mkdir -p {target} && '
708
+ 'rsync -avzrhP {filters}{source} {target}/'
709
+ ).format(
710
+ target=quoted_target,
711
+ filters=rsync_filters,
712
+ source=quoted_result,
692
713
  )
693
714
 
694
- sync_cmd = (
695
- f"rsync -avzrhP {rsync_filters}"
696
- f"{quoted_result} {quoted_prod}"
715
+ def sbatch_command(job_name, stdout_name, stderr_name, target_folder):
716
+ wrap_cmd = sync_command(target_folder)
717
+ return (
718
+ 'sbatch --wait -p datamover --mem=10G --time 12:00:00 '
719
+ '-J {job_name} --mail-user=pdb_em@ebi.ac.uk '
720
+ '--mail-type=END --mail-type=FAIL '
721
+ '-o {stdout_path} -e {stderr_path} --wrap="{wrap_cmd}"'
722
+ ).format(
723
+ job_name=shlex.quote(job_name),
724
+ stdout_path=shlex.quote('{}{}'.format(result_folder, stdout_name)),
725
+ stderr_path=shlex.quote('{}{}'.format(result_folder, stderr_name)),
726
+ wrap_cmd=wrap_cmd,
697
727
  )
698
- prod_stdout = f"{result_folder}{id}_prodsync_stdout.txt"
699
- prod_stderr = f"{result_folder}{id}_prodsync_stderr.txt"
700
-
701
- # for codon staging, production and dev
702
- try:
703
- subprocess.check_call(['ssh', 'wp-p1m2-e1',
704
- 'ls ' + prod_folder],
705
- stdout=subprocess.PIPE, stderr=subprocess.PIPE)
706
- prod_rc = self.run_shell_command(sync_cmd, prod_stdout, prod_stderr)
707
- if prod_rc == 0:
708
- logging.info('Target folder exists and data sync to production has completed')
709
- else:
710
- logging.info('Target folder exists but data sync to production failed')
711
-
712
- # sync data to HL
713
- fallback_prod_folder = prod_folder
714
- quoted_fallback_prod_folder = shlex.quote(fallback_prod_folder)
715
- quoted_fallback_source = shlex.quote(fallback_prod_folder.rstrip('/') + '/')
716
- quoted_fallback_dest = 'hl2-ws-login:' + shlex.quote(fallback_prod_folder.rstrip('/') + '/')
717
-
718
- fallback_sync_cmd = (
719
- f"ssh hl2-ws-login 'mkdir -p {quoted_fallback_prod_folder}' && "
720
- f"rsync -avzrhP {rsync_filters}"
721
- f"{quoted_fallback_source} {quoted_fallback_dest}"
722
- )
723
- fallback_stdout = f"{result_folder}{id}_fallbacksync_stdout.txt"
724
- fallback_stderr = f"{result_folder}{id}_fallbacksync_stderr.txt"
725
- fallback_rc = self.run_shell_command(fallback_sync_cmd, fallback_stdout, fallback_stderr)
726
- if fallback_rc == 0:
727
- logging.info("Fallback validation_analysis sync has completed")
728
- else:
729
- logging.info("Fallback validation_analysis sync failed")
730
728
 
731
- except subprocess.CalledProcessError as e:
732
- try:
733
- subprocess.check_call(['ssh wp-p1m2-e1 "mkdir -p ' + prod_folder + '"'], stdout=subprocess.PIPE,
734
- stderr=subprocess.PIPE, shell=True)
735
- prod_rc = self.run_shell_command(sync_cmd, prod_stdout, prod_stderr)
736
- if prod_rc == 0:
737
- logging.info('Target folder was created and data sync to production has completed')
738
- else:
739
- logging.info('Target folder was created but data sync to production failed')
740
-
741
- # sync data to HL
742
- fallback_prod_folder = prod_folder
743
- quoted_fallback_prod_folder = shlex.quote(fallback_prod_folder)
744
- quoted_fallback_source = shlex.quote(fallback_prod_folder.rstrip('/') + '/')
745
- quoted_fallback_dest = 'hl2-ws-login:' + shlex.quote(fallback_prod_folder.rstrip('/') + '/')
746
-
747
- fallback_sync_cmd = (
748
- f"ssh hl2-ws-login 'mkdir -p {quoted_fallback_prod_folder}' && "
749
- f"rsync -avzrhP {rsync_filters}"
750
- f"{quoted_fallback_source} {quoted_fallback_dest}"
751
- )
752
- fallback_stdout = f"{result_folder}{id}_fallbacksync_stdout.txt"
753
- fallback_stderr = f"{result_folder}{id}_fallbacksync_stderr.txt"
754
- fallback_rc = self.run_shell_command(fallback_sync_cmd, fallback_stdout, fallback_stderr)
755
- if fallback_rc == 0:
756
- logging.info("Fallback validation_analysis sync has completed")
757
- else:
758
- logging.info("Fallback validation_analysis sync failed")
759
-
760
- except subprocess.CalledProcessError as e:
761
- logging.info('Target folder not properly produced.')
762
- else:
763
- logging.info('No all json output, job failed to the end')
729
+ sync_jobs = [
730
+ (
731
+ 'production',
732
+ sbatch_command(
733
+ 'vasync_{}'.format(id),
734
+ '{}_prodsync_stdout.txt'.format(id),
735
+ '{}_prodsync_stderr.txt'.format(id),
736
+ prod_folder,
737
+ ),
738
+ ),
739
+ (
740
+ 'fallback',
741
+ sbatch_command(
742
+ 'vafallbacksync_{}'.format(id),
743
+ '{}_fallbacksync_stdout.txt'.format(id),
744
+ '{}_fallbacksync_stderr.txt'.format(id),
745
+ fallback_prod_folder,
746
+ ),
747
+ ),
748
+ ]
749
+
750
+ for label, job_cmd in sync_jobs:
751
+ logging.info('Submitting %s sync job...', label)
752
+ logging.info(job_cmd)
753
+ subprocess.check_call(
754
+ ['ssh', 'codon-slurm-login', job_cmd],
755
+ stdout=subprocess.PIPE,
756
+ stderr=subprocess.PIPE,
757
+ )
758
+ logging.info('%s sync job has been submitted', label)
764
759
 
765
760
  return None
766
761
 
@@ -1069,7 +1064,6 @@ def main():
1069
1064
 
1070
1065
  if not myobj.onlycopy and not myobj.onlysync:
1071
1066
  myobj.runcmd()
1072
- myobj.data_sync()
1073
1067
  else:
1074
1068
  logging.info('Only copy files. Please make sure use --update t/Ture/1')
1075
1069