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.
- {emdbva-0.0.1.dev144/emdbva.egg-info → emdbva-0.0.1.dev146}/PKG-INFO +1 -1
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146/emdbva.egg-info}/PKG-INFO +1 -1
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/emdbva.egg-info/SOURCES.txt +2 -0
- emdbva-0.0.1.dev146/va/audit/__init__.py +13 -0
- emdbva-0.0.1.dev146/va/audit/cif_revision.py +324 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/mainva.py +13 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/resmap.py +7 -7
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/prepareandrun_codon_airflow.py +78 -84
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/schemas/emdb_entry_full.schema.json +160 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/utils/misc.py +63 -37
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/utils/stars.py +286 -93
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/validationanalysis.py +24 -13
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/version.py +1 -1
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/LICENSE +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/MANIFEST.in +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/README.rst +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/emdbva.egg-info/dependency_links.txt +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/emdbva.egg-info/entry_points.txt +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/emdbva.egg-info/requires.txt +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/emdbva.egg-info/top_level.txt +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/setup.cfg +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/setup.py +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/PATHS_codon_airflow.py +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/__init__.py +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/__init__.py +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/bars.py +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/connected_percentage.py +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/contour_level_predicator.py +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/emda_mmcc.py +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/emringer.py +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/inclusion.py +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/map_data_validation.py +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/overlap_percentage.py +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/phaserandomization.py +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/phenix_cc.py +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/phenix_mm.py +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/projections.py +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/qscore.py +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/residue_locres.py +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/smoc.py +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/strudel.py +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/surfaces.py +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/metrics/threedfsc.py +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/preparation.py +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/qscores.csv +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/schemas/__init__.py +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/schemas/loader.py +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/utils/Checker.py +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/utils/ChimeraxViews.py +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/utils/MapProcessor.py +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/utils/Model.py +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/utils/__init__.py +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/utils/cl_weights.pth +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/utils/log_utils.py +0 -0
- {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev146}/va/utils/rescolor.py +0 -0
|
@@ -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
|
|
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 =
|
|
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
|
|
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
|
|
311
|
+
print('Animated PNG for local resolution result has been produced.')
|
|
312
312
|
except subprocess.CalledProcessError as suberr:
|
|
313
|
-
err = 'Saving
|
|
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.
|
|
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
|
|
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
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|
|