emdbva 0.0.1.dev128__tar.gz → 0.0.1.dev130__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 (47) hide show
  1. {emdbva-0.0.1.dev128/emdbva.egg-info → emdbva-0.0.1.dev130}/PKG-INFO +1 -1
  2. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130/emdbva.egg-info}/PKG-INFO +1 -1
  3. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/va/preparation.py +38 -5
  4. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/va/utils/ChimeraxViews.py +10 -3
  5. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/va/utils/stars.py +174 -1
  6. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/va/validationanalysis.py +175 -23
  7. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/va/version.py +1 -1
  8. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/LICENSE +0 -0
  9. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/MANIFEST.in +0 -0
  10. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/README.rst +0 -0
  11. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/emdbva.egg-info/SOURCES.txt +0 -0
  12. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/emdbva.egg-info/dependency_links.txt +0 -0
  13. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/emdbva.egg-info/entry_points.txt +0 -0
  14. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/emdbva.egg-info/requires.txt +0 -0
  15. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/emdbva.egg-info/top_level.txt +0 -0
  16. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/setup.cfg +0 -0
  17. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/setup.py +0 -0
  18. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/va/__init__.py +0 -0
  19. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/va/mainva.py +0 -0
  20. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/va/metrics/__init__.py +0 -0
  21. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/va/metrics/bars.py +0 -0
  22. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/va/metrics/connected_percentage.py +0 -0
  23. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/va/metrics/contour_level_predicator.py +0 -0
  24. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/va/metrics/emda_mmcc.py +0 -0
  25. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/va/metrics/emringer.py +0 -0
  26. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/va/metrics/inclusion.py +0 -0
  27. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/va/metrics/overlap_percentage.py +0 -0
  28. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/va/metrics/phaserandomization.py +0 -0
  29. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/va/metrics/phenix_cc.py +0 -0
  30. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/va/metrics/phenix_mm.py +0 -0
  31. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/va/metrics/projections.py +0 -0
  32. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/va/metrics/qscore.py +0 -0
  33. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/va/metrics/residue_locres.py +0 -0
  34. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/va/metrics/resmap.py +0 -0
  35. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/va/metrics/smoc.py +0 -0
  36. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/va/metrics/strudel.py +0 -0
  37. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/va/metrics/surfaces.py +0 -0
  38. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/va/metrics/threedfsc.py +0 -0
  39. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/va/qscores.csv +0 -0
  40. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/va/utils/Checker.py +0 -0
  41. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/va/utils/MapProcessor.py +0 -0
  42. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/va/utils/Model.py +0 -0
  43. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/va/utils/__init__.py +0 -0
  44. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/va/utils/cl_weights.pth +0 -0
  45. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/va/utils/log_utils.py +0 -0
  46. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/va/utils/misc.py +0 -0
  47. {emdbva-0.0.1.dev128 → emdbva-0.0.1.dev130}/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.dev128
3
+ Version: 0.0.1.dev130
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.dev128
3
+ Version: 0.0.1.dev130
4
4
  Summary: CryoEM validation toolkit
5
5
  Home-page: https://test.pypi.org/project/va/
6
6
  Author: Zhe Wang
@@ -669,12 +669,44 @@ class PreParation:
669
669
  block = doc.sole_block()
670
670
 
671
671
  # Extract resolution and reconstruction method
672
- reconstruction_method = block.find_value('_em_experiment.reconstruction_method').replace("'", "").lower() or None
673
- resolution = block.find_value('_em_3d_reconstruction.resolution')
674
- resolution = float(resolution) if resolution and resolution != '?' else None
672
+ rm = block.find_value('_em_experiment.reconstruction_method')
673
+ reconstruction_method = (
674
+ str(rm).replace("'", "").strip().lower()
675
+ if rm and str(rm).strip() not in ('?', '.', '')
676
+ else None
677
+ )
678
+
679
+ # Extract resolution (scalar)
680
+ res = block.find_value('_em_3d_reconstruction.resolution')
681
+ resolution = None
682
+ if res and str(res).strip() not in ('?', '.', ''):
683
+ try:
684
+ resolution = float(str(res).strip().strip("'").strip('"'))
685
+ except ValueError:
686
+ resolution = None
687
+
688
+ # Fallback: try loop column *only if it exists*
675
689
  if resolution is None and reconstruction_method != 'tomography':
676
- resolution_loop = block.find_loop('_em_3d_reconstruction.resolution')
677
- resolution = next(x for x in resolution_loop if x != '?')
690
+ col = block.find_loop('_em_3d_reconstruction.resolution') # returns Column
691
+
692
+ # If the column is nil, get_loop() will either fail or be useless.
693
+ # So we just try to iterate safely:
694
+ value = None
695
+ try:
696
+ for x in col: # iterates over column values if non-nil
697
+ sx = str(x).strip().strip("'").strip('"')
698
+ if sx not in ('?', '.', ''):
699
+ value = sx
700
+ break
701
+ except Exception:
702
+ value = None
703
+
704
+ if value is not None:
705
+ try:
706
+ resolution = float(value)
707
+ except ValueError:
708
+ resolution = None
709
+
678
710
 
679
711
  # Initialize map containers
680
712
  map_data = {'primary': {}, 'mask': {}, 'odd': {}, 'even': {}}
@@ -701,6 +733,7 @@ class PreParation:
701
733
  map_types = [map_type]
702
734
  except Exception as e:
703
735
  # fallback or log error
736
+ print(f'Error parsing map information: {e}')
704
737
  contour_levels, map_files, map_types = [], [], []
705
738
 
706
739
  # Parse combined map data
@@ -475,11 +475,17 @@ class ChimeraxViews:
475
475
  output_json = {}
476
476
  raw_map = f'{self.va_dir}/{map_name[:-4] + "_rawmap.map"}'
477
477
  mask_map = f'{self.va_dir}/{map_name}_relion/mask/{map_name}_mask.mrc'
478
+ # if mask does not exist, treat as no mask
479
+ if not os.path.isfile(mask_map):
480
+ mask_map = None
478
481
  local_resolution_map_glob = glob.glob(f'{self.va_dir}/{map_name}_relion/*_locres.mrc')
479
482
  local_resolution_map = local_resolution_map_glob[0] if len(local_resolution_map_glob) > 0 else None
480
483
  map_processor = MapProcessor()
481
- binarized_mask_map = map_processor.binarized_mask(mask_map, map_name)
482
- masked_raw_map = map_processor.mask_map(raw_map, binarized_mask_map)
484
+ if mask_map:
485
+ binarized_mask_map = map_processor.binarized_mask(mask_map, map_name)
486
+ masked_raw_map = map_processor.mask_map(raw_map, binarized_mask_map)
487
+ else:
488
+ masked_raw_map = raw_map
483
489
  map_min, map_max = map_processor.map_minmax(masked_raw_map, local_resolution_map)
484
490
  real_min = map_min if map_min < all_models_min_value else all_models_min_value
485
491
  real_max = map_max if map_max > all_models_max_value else all_models_max_value
@@ -487,7 +493,8 @@ class ChimeraxViews:
487
493
  new_max = scale_value(map_max, real_min, real_max, 0, 1)
488
494
  num_elements = 50
489
495
  palette_colours = self.generate_palette(new_min, new_max, map_min, map_max, num_elements)
490
- chimerax_file_name = self.write_maps_cxc(os.path.basename(masked_raw_map), local_resolution_map, palette_colours, data_type)
496
+ chimerax_file_name = self.write_maps_cxc(os.path.basename(masked_raw_map), local_resolution_map,
497
+ palette_colours, data_type)
491
498
  out = self.run_chimerax(chimerax_file_name)
492
499
  surfaces_dict = self.rescale_view(os.path.basename(masked_raw_map), None, None, data_type)
493
500
  output_json[f'{data_type}_views'] = surfaces_dict
@@ -64,6 +64,12 @@ class GetStars:
64
64
 
65
65
  return float(self.data_general_block()['_rlnFinalResolution'])
66
66
 
67
+ def bfactor_sharpen(self):
68
+ """
69
+ Get the resolution value that Relion is use for the phase randomization calculation
70
+ """
71
+ return float(self.data_general_block()['_rlnBfactorUsedForSharpening'])
72
+
67
73
  def data_fsc_block(self):
68
74
  """
69
75
  Get the data_fsc block
@@ -89,6 +95,73 @@ class GetStars:
89
95
 
90
96
  return data_fsc_block
91
97
 
98
+ def data_guinier_block(self):
99
+ """
100
+ Get the data_guinier block (RELION postprocess output).
101
+
102
+ Returns:
103
+ dict: { column_name(str): [values...] }
104
+ Example keys (typical RELION):
105
+ - ResolutionSquared
106
+ - LogAmplitudesOriginal
107
+ - LogAmplitudesWeighted
108
+ - LogAmplitudesSharpened
109
+ - LogAmplitudesIntercept
110
+ """
111
+ if 'data_guinier' not in self.star_blocks:
112
+ return {}
113
+
114
+ lines = [l.strip() for l in self.star_blocks['data_guinier'] if l.strip()]
115
+ if not lines:
116
+ return {}
117
+
118
+ # Find loop_ line
119
+ try:
120
+ loop_idx = lines.index('loop_')
121
+ except ValueError:
122
+ # Sometimes it could be "loop_" with trailing spaces already stripped above,
123
+ # or the block could be non-loop (rare). In that case, return empty for now.
124
+ return {}
125
+
126
+ # Read headers: consecutive lines starting with "_rln"
127
+ header_lines = []
128
+ i = loop_idx + 1
129
+ while i < len(lines) and lines[i].startswith('_rln'):
130
+ header_lines.append(lines[i])
131
+ i += 1
132
+
133
+ # Convert header lines to clean names (strip "_rln" and remove "#n")
134
+ headers = []
135
+ for h in header_lines:
136
+ # e.g. "_rlnResolutionSquared #1" -> "ResolutionSquared"
137
+ parts = h.split()
138
+ tag = parts[0] # "_rlnResolutionSquared"
139
+ headers.append(tag[4:]) # remove "_rln"
140
+
141
+ # Remaining lines are data rows
142
+ data_rows = lines[i:]
143
+ if not data_rows or not headers:
144
+ return {}
145
+
146
+ # Parse rows into columns
147
+ col_values = [[] for _ in headers]
148
+ for row in data_rows:
149
+ vals = row.split()
150
+ if len(vals) < len(headers):
151
+ # skip malformed/short row
152
+ continue
153
+ for ci in range(len(headers)):
154
+ v = vals[ci]
155
+ # RELION usually uses numeric values; be defensive anyway
156
+ try:
157
+ fv = float(v)
158
+ col_values[ci].append(keep_three_significant_digits(fv))
159
+ except Exception:
160
+ col_values[ci].append(v)
161
+
162
+ return dict(zip(headers, col_values))
163
+
164
+
92
165
  def data_extra(self, data_fsc_block):
93
166
  """
94
167
  Add half bit, one-bit and 3-sigma to data_fsc_block
@@ -148,6 +221,7 @@ class GetStars:
148
221
  corrected = data_fsc_block['FourierShellCorrelationCorrected']
149
222
  phase_rand = data_fsc_block.get('CorrectedFourierShellCorrelationPhaseRandomizedMaskedMaps', None)
150
223
  randomise_from = self.randomise_from()
224
+ sharpen_bfactor = self.bfactor_sharpen()
151
225
 
152
226
  x_gold, y_gold = interpolated_intercept(x, correlation, gold)
153
227
  x_half, y_half = interpolated_intercept(x, correlation, half)
@@ -214,7 +288,8 @@ class GetStars:
214
288
  'phase_randomized_0.143': {'x': x_phase_gold, 'y': y_phase_gold},
215
289
  'phase_randomized_half': {'x': x_phase_half, 'y': y_phase_half},
216
290
  'phase_randomized_halfbit': {'x': x_phase_halfbit, 'y': y_phase_halfbit},
217
- 'randomise_from': randomise_from
291
+ 'randomise_from': randomise_from,
292
+ 'sharpen_bfactor': sharpen_bfactor
218
293
  }
219
294
  }
220
295
 
@@ -240,6 +315,14 @@ class GetStars:
240
315
  'fsc_corrected': 'FourierShellCorrelationCorrected'
241
316
  }
242
317
  curves = {key: data_fsc_block[value] for key, value in curve_mapping.items() if value in data_fsc_block}
318
+
319
+ # incorporate any guinier curves returned by data_guinier_block()
320
+ guinier = self.data_guinier_block()
321
+ if guinier:
322
+ # add guinier columns into curves; avoid overwriting existing keys
323
+ for gk, gv in guinier.items():
324
+ curves[f'guinier_{str(gk).lower()}'] = gv
325
+
243
326
  final_curves = {'curves': curves}
244
327
 
245
328
  return final_curves
@@ -315,6 +398,7 @@ class GetStars:
315
398
  plt.plot(levels, golden_line, linestyle='-.', label='0.143')
316
399
  plt.legend(loc='upper right', fontsize='x-small')
317
400
  plt.savefig(self.va_dir + '/current_Relion_fsc.png')
401
+ print(f'FSC curve saved to {self.va_dir}/current_Relion_fsc.png')
318
402
  plt.close()
319
403
 
320
404
  def plot_feature_zone(self, intersections, levels, corrected_curve, phase_rand_curve):
@@ -365,6 +449,95 @@ class GetStars:
365
449
  plt.close()
366
450
 
367
451
  return feature_area, overfit_area
452
+
453
+ def plot_guinier(self, guinier_block=None, save_path=None, show=False):
454
+ """
455
+ Plot Guinier curves (Log amplitudes vs ResolutionSquared).
456
+
457
+ Parameters:
458
+ guinier_block (dict): output from self.data_guinier_block(). If None, the method will call it.
459
+ save_path (str): full path to save the image. If None, saves to `self.va_dir/current_Relion_guinier.png`.
460
+ show (bool): if True, call plt.show() before closing.
461
+ Returns:
462
+ str or None: path to saved image or None if no data.
463
+ """
464
+ if guinier_block is None:
465
+ guinier_block = self.data_guinier_block()
466
+ if not guinier_block:
467
+ print('No guinier data to plot')
468
+ return None
469
+
470
+ # prefer canonical key name
471
+ x_key = None
472
+ for candidate in ('ResolutionSquared', 'resolutionSquared', 'Resolution_Squared', 'Resolution'):
473
+ if candidate in guinier_block:
474
+ x_key = candidate
475
+ break
476
+ if x_key is None:
477
+ print('No resolution column found in guinier data')
478
+ return None
479
+
480
+ try:
481
+ x = np.array(guinier_block[x_key], dtype=float)
482
+ except Exception:
483
+ print('Resolution column contains non-numeric data')
484
+ return None
485
+
486
+ # detect weighted column to filter out -99 sentinel rows
487
+ weighted_key = next((k for k in guinier_block.keys() if 'weighted' in k.lower()), None)
488
+ mask = None
489
+ if weighted_key is not None:
490
+ try:
491
+ weighted_arr = np.array(guinier_block[weighted_key], dtype=float)
492
+ mask = weighted_arr != -99
493
+ if not np.any(mask):
494
+ print('All weighted values are -99; no guinier data to plot')
495
+ return None
496
+ # apply mask to x
497
+ x = x[mask]
498
+ except Exception:
499
+ mask = None
500
+
501
+ # pick numeric columns excluding the x column
502
+ y_keys = [k for k in guinier_block.keys() if k != x_key]
503
+ if not y_keys:
504
+ print('No guinier amplitude columns to plot')
505
+ return None
506
+
507
+ plt.figure(figsize=(6, 4))
508
+ for key in y_keys:
509
+ try:
510
+ y = np.array(guinier_block[key], dtype=float)
511
+ except Exception:
512
+ # skip non-numeric series
513
+ continue
514
+ if mask is not None:
515
+ # ensure same length before applying mask
516
+ if y.shape[0] != mask.shape[0]:
517
+ # skip series with mismatched length
518
+ continue
519
+ y = y[mask]
520
+ if y.size == 0 or x.size == 0 or x.shape[0] != y.shape[0]:
521
+ # skip empty or mismatched series
522
+ continue
523
+ plt.plot(x, y, linestyle='-', label=key)
524
+
525
+ plt.xlabel(x_key)
526
+ plt.ylabel('Log Amplitude')
527
+ plt.title('Guinier plot')
528
+ plt.legend(loc='best', fontsize='x-small')
529
+ plt.grid(True)
530
+
531
+ out_path = save_path or f'{self.va_dir}/current_Relion_guinier.png'
532
+ plt.tight_layout()
533
+ plt.savefig(out_path)
534
+ if show:
535
+ plt.show()
536
+ plt.close()
537
+ print(f'Guinier plot saved to {out_path}')
538
+ return out_path
539
+
540
+
368
541
  @staticmethod
369
542
  def area_difference(curve_one, curve_two, levels):
370
543
  """
@@ -293,7 +293,11 @@ class ValidationAnalysis:
293
293
  star = GetStars(star_file_dir)
294
294
  nomask_resolution = star.final_resolution()
295
295
  # For cases where Relion resolution is too high, use the input resolution
296
- nomask_resolution = min(nomask_resolution, 15 * float(self.resolution))
296
+ if self.resolution is not None:
297
+ try:
298
+ nomask_resolution = min(nomask_resolution, 15 * float(self.resolution))
299
+ except (TypeError, ValueError):
300
+ pass
297
301
  raw_map_name = find_rawmap_file(self.workdir)
298
302
  filtered_raw_map = self.get_lowpassed_map(f'{self.workdir}/{raw_map_name}', nomask_resolution)
299
303
  relion_mask_name = relion_mask(filtered_raw_map, relion_mask_dir, self.mapname)
@@ -3845,6 +3849,7 @@ class ValidationAnalysis:
3845
3849
  star.plot_fsc(data_curves, (xlist, ylist))
3846
3850
  else:
3847
3851
  star.plot_fsc(data_curves)
3852
+ star.plot_guinier()
3848
3853
  data_intersections = star.all_intersection(data_fsc_block)
3849
3854
 
3850
3855
  fsc_dict = {**data_curves, **data_intersections, **zones}
@@ -6464,93 +6469,233 @@ class ValidationAnalysis:
6464
6469
  def phenix_mmfsc(self):
6465
6470
  """
6466
6471
  Run Phenix map-model FSC calculation
6472
+ Now also supports:
6473
+ - raw map (existing)
6474
+ - half map odd
6475
+ - half map even
6467
6476
  :return:
6468
6477
  """
6469
6478
 
6470
- if self.platform == 'emdb' and self.met != 'tomo' and self.met != 'crys' :
6479
+ if self.platform == 'emdb' and self.met != 'tomo' and self.met != 'crys':
6471
6480
  start = timeit.default_timer()
6472
6481
  errlist = []
6482
+
6473
6483
  result_dict = {}
6474
6484
  raw_result_dict = {}
6475
- # if strudelapp and self.models:
6485
+ odd_result_dict = {}
6486
+ even_result_dict = {}
6487
+
6476
6488
  if self.models:
6477
6489
  nummodels = 0
6490
+
6478
6491
  allresults = {}
6479
6492
  raw_allresults = {}
6493
+ odd_allresults = {}
6494
+ even_allresults = {}
6495
+
6480
6496
  for model in self.models:
6497
+ # prepare some vars so they always exist
6498
+ rerr = None
6499
+ mmfsclog = None
6500
+
6501
+ raw_rerr = None
6502
+ raw_mmfsclog = None
6503
+
6504
+ odd_rerr = None
6505
+ odd_mmfsclog = None
6506
+
6507
+ even_rerr = None
6508
+ even_mmfsclog = None
6509
+
6481
6510
  try:
6482
6511
  full_model = '{}{}'.format(self.workdir, os.path.basename(model.filename))
6483
6512
  full_map = '{}{}'.format(self.workdir, self.mapname)
6484
6513
  out_path = '{}_phenix'.format(full_model)
6514
+
6515
+ # -------------------------
6516
+ # Primary map mmFSC (existing)
6517
+ # -------------------------
6485
6518
  rerr = run_phenixmmfsc(full_model, full_map, out_path)
6486
6519
  tmmfsclog = '{}/fsc_model.unmasked.mtriage.log'.format(out_path)
6487
6520
  mmfsclog = '{}/fsc_model.unmasked.mtriage_{}.log'.format(out_path, self.mapname)
6488
6521
  change_filename(tmmfsclog, mmfsclog)
6489
- raw_mmfsclog = None
6490
- raw_rerr = None
6522
+
6523
+ # -------------------------
6524
+ # Raw map mmFSC (existing behavior)
6525
+ # NOTE: your code checks self.hmodd and self.hmeven to decide raw.
6526
+ # If you actually mean "if rawmap exists", replace condition with "if self.rawmap"
6527
+ # -------------------------
6491
6528
  if self.hmodd and self.hmeven:
6492
6529
  raw_full_map = self.rawmap.fullname
6493
6530
  raw_mapname = os.path.basename(raw_full_map)
6494
6531
  raw_rerr = run_phenixmmfsc(full_model, raw_full_map, out_path)
6532
+ tmmfsclog = '{}/fsc_model.unmasked.mtriage.log'.format(out_path)
6495
6533
  raw_mmfsclog = '{}/fsc_model.unmasked.mtriage_{}.log'.format(out_path, raw_mapname)
6496
6534
  change_filename(tmmfsclog, raw_mmfsclog)
6535
+
6536
+ # -------------------------
6537
+ # Half-map odd/even mmFSC (NEW)
6538
+ # -------------------------
6539
+ if self.hmodd and self.hmeven:
6540
+ # odd
6541
+ odd_full_map = getattr(self.hmodd, "fullname", self.hmodd)
6542
+ odd_mapname = os.path.basename(odd_full_map)
6543
+ odd_rerr = run_phenixmmfsc(full_model, odd_full_map, out_path)
6544
+ tmmfsclog = '{}/fsc_model.unmasked.mtriage.log'.format(out_path)
6545
+ odd_mmfsclog = '{}/fsc_model.unmasked.mtriage_{}.log'.format(out_path, odd_mapname)
6546
+ change_filename(tmmfsclog, odd_mmfsclog)
6547
+
6548
+ # even
6549
+ even_full_map = getattr(self.hmeven, "fullname", self.hmeven)
6550
+ even_mapname = os.path.basename(even_full_map)
6551
+ even_rerr = run_phenixmmfsc(full_model, even_full_map, out_path)
6552
+ tmmfsclog = '{}/fsc_model.unmasked.mtriage.log'.format(out_path)
6553
+ even_mmfsclog = '{}/fsc_model.unmasked.mtriage_{}.log'.format(out_path, even_mapname)
6554
+ change_filename(tmmfsclog, even_mmfsclog)
6555
+
6497
6556
  end = timeit.default_timer()
6498
6557
  print('Phenix mmFSC time: %s' % (end - start))
6499
6558
  print('------------------------------------')
6559
+
6500
6560
  except:
6501
6561
  err = 'Phenix mmFSC calculation error: {}.'.format(sys.exc_info()[1])
6502
6562
  errlist.append(err)
6503
6563
  sys.stderr.write(err + '\n')
6564
+
6504
6565
  nyquist = 1 / (2 * self.map.voxel_size.tolist()[0])
6505
- try:
6506
- df = read_mmfsc(mmfsclog, rerr)
6507
- mmfscdict = mmfscdf_todict(df, nyquist)
6508
6566
 
6509
- allresults[str(nummodels)] = {'name': os.path.basename(model.filename),
6510
- 'data': mmfscdict}
6567
+ # -------------------------
6568
+ # Parse + collect results (Primary)
6569
+ # -------------------------
6570
+ try:
6571
+ if mmfsclog is not None and rerr is not None:
6572
+ df = read_mmfsc(mmfsclog, rerr)
6573
+ mmfscdict = mmfscdf_todict(df, nyquist)
6574
+
6575
+ allresults[str(nummodels)] = {
6576
+ 'name': os.path.basename(model.filename),
6577
+ 'data': mmfscdict
6578
+ }
6579
+ except:
6580
+ err = 'Save Phenix mmFSC data error: {}.'.format(sys.exc_info()[1])
6581
+ errlist.append(err)
6582
+ sys.stderr.write(err + '\n')
6511
6583
 
6512
- if self.hmodd and self.hmeven:
6584
+ # -------------------------
6585
+ # Parse + collect results (Raw)
6586
+ # -------------------------
6587
+ try:
6588
+ if self.hmodd and self.hmeven and raw_mmfsclog is not None and raw_rerr is not None:
6513
6589
  raw_df = read_mmfsc(raw_mmfsclog, raw_rerr)
6514
6590
  raw_mmfscdict = mmfscdf_todict(raw_df, nyquist)
6515
6591
 
6516
- raw_allresults[str(nummodels)] = {'name': os.path.basename(model.filename),
6517
- 'data': raw_mmfscdict}
6592
+ raw_allresults[str(nummodels)] = {
6593
+ 'name': os.path.basename(model.filename),
6594
+ 'data': raw_mmfscdict
6595
+ }
6596
+ except:
6597
+ err = 'Save raw Phenix mmFSC data error: {}.'.format(sys.exc_info()[1])
6598
+ errlist.append(err)
6599
+ sys.stderr.write(err + '\n')
6600
+
6601
+ # -------------------------
6602
+ # Parse + collect results (Half odd) NEW
6603
+ # -------------------------
6604
+ try:
6605
+ if self.hmodd and self.hmeven and odd_mmfsclog is not None and odd_rerr is not None:
6606
+ odd_df = read_mmfsc(odd_mmfsclog, odd_rerr)
6607
+ odd_mmfscdict = mmfscdf_todict(odd_df, nyquist)
6608
+
6609
+ odd_allresults[str(nummodels)] = {
6610
+ 'name': os.path.basename(model.filename),
6611
+ 'data': odd_mmfscdict
6612
+ }
6613
+ except:
6614
+ err = 'Save half-odd Phenix mmFSC data error: {}.'.format(sys.exc_info()[1])
6615
+ errlist.append(err)
6616
+ sys.stderr.write(err + '\n')
6518
6617
 
6618
+ # -------------------------
6619
+ # Parse + collect results (Half even) NEW
6620
+ # -------------------------
6621
+ try:
6622
+ if self.hmodd and self.hmeven and even_mmfsclog is not None and even_rerr is not None:
6623
+ even_df = read_mmfsc(even_mmfsclog, even_rerr)
6624
+ even_mmfscdict = mmfscdf_todict(even_df, nyquist)
6625
+
6626
+ even_allresults[str(nummodels)] = {
6627
+ 'name': os.path.basename(model.filename),
6628
+ 'data': even_mmfscdict
6629
+ }
6519
6630
  except:
6520
- err = 'Save Phenix mmFSC data error: {}.'.format(sys.exc_info()[1])
6631
+ err = 'Save half-even Phenix mmFSC data error: {}.'.format(sys.exc_info()[1])
6521
6632
  errlist.append(err)
6522
6633
  sys.stderr.write(err + '\n')
6634
+
6523
6635
  nummodels += 1
6524
6636
 
6637
+ # -------------------------
6638
+ # Build output dicts (same pattern you used)
6639
+ # -------------------------
6525
6640
  if allresults:
6526
6641
  result_dict['mmfsc'] = allresults
6642
+
6527
6643
  if raw_allresults:
6528
6644
  raw_result_dict['raw_mmfsc'] = raw_allresults
6529
6645
 
6646
+ if odd_allresults:
6647
+ odd_result_dict['half_odd_mmfsc'] = odd_allresults
6648
+
6649
+ if even_allresults:
6650
+ even_result_dict['half_even_mmfsc'] = even_allresults
6651
+
6530
6652
  if errlist:
6653
+ # keep your behavior: errors only attached to result_dict
6531
6654
  result_dict['err'] = errlist
6532
6655
 
6656
+ # -------------------------
6657
+ # Write JSON files (same gating logic you used)
6658
+ # -------------------------
6533
6659
  if len(result_dict) == 1 and 'mmfsc' in result_dict:
6534
6660
  try:
6535
- with codecs.open(self.workdir + self.mapname + '_mmfsc.json', 'w',
6536
- encoding='utf-8') as f:
6661
+ with codecs.open(self.workdir + self.mapname + '_mmfsc.json', 'w', encoding='utf-8') as f:
6537
6662
  json.dump(result_dict, f)
6538
6663
  except:
6539
6664
  sys.stderr.write('Saving to mmFSC json error: {}.\n'.format(sys.exc_info()[1]))
6540
-
6541
6665
  else:
6542
6666
  print('mmFSC was not collected, please check the error!')
6543
6667
 
6544
6668
  if len(raw_result_dict) == 1 and 'raw_mmfsc' in raw_result_dict:
6545
6669
  try:
6546
- with codecs.open(self.workdir + self.mapname + '_raw_mmfsc.json', 'w',
6547
- encoding='utf-8') as f:
6670
+ with codecs.open(self.workdir + self.mapname + '_raw_mmfsc.json', 'w', encoding='utf-8') as f:
6548
6671
  json.dump(raw_result_dict, f)
6549
6672
  except:
6550
6673
  sys.stderr.write('Saving to raw mmFSC json error: {}.\n'.format(sys.exc_info()[1]))
6674
+ else:
6675
+ if self.hmodd and self.hmeven:
6676
+ print('Raw map mmFSC was not collected, please check the error!')
6677
+
6678
+ if len(odd_result_dict) == 1 and 'half_odd_mmfsc' in odd_result_dict:
6679
+ try:
6680
+ with codecs.open(self.workdir + self.mapname + '_half_odd_mmfsc.json', 'w',
6681
+ encoding='utf-8') as f:
6682
+ json.dump(odd_result_dict, f)
6683
+ except:
6684
+ sys.stderr.write('Saving to half-odd mmFSC json error: {}.\n'.format(sys.exc_info()[1]))
6685
+ else:
6686
+ if self.hmodd and self.hmeven:
6687
+ print('Half-odd map mmFSC was not collected, please check the error!')
6551
6688
 
6689
+ if len(even_result_dict) == 1 and 'half_even_mmfsc' in even_result_dict:
6690
+ try:
6691
+ with codecs.open(self.workdir + self.mapname + '_half_even_mmfsc.json', 'w',
6692
+ encoding='utf-8') as f:
6693
+ json.dump(even_result_dict, f)
6694
+ except:
6695
+ sys.stderr.write('Saving to half-even mmFSC json error: {}.\n'.format(sys.exc_info()[1]))
6552
6696
  else:
6553
- print('Raw map mmFSC was not collected, please check the error!')
6697
+ if self.hmodd and self.hmeven:
6698
+ print('Half-even map mmFSC was not collected, please check the error!')
6554
6699
 
6555
6700
  else:
6556
6701
  print('No model!!')
@@ -6558,6 +6703,7 @@ class ValidationAnalysis:
6558
6703
 
6559
6704
  return None
6560
6705
 
6706
+
6561
6707
  @profile_peak_memory()
6562
6708
  def locres_resmap(self):
6563
6709
  """
@@ -6633,10 +6779,16 @@ class ValidationAnalysis:
6633
6779
  result_dict = {}
6634
6780
  errlist = []
6635
6781
  start = timeit.default_timer()
6782
+ local_res = None
6636
6783
  if self.platform == 'emdb' and self.met != 'tomo' and self.hmodd and self.hmeven:
6637
- local_res = localres_histogram(self.hmodd.fullname, self.mapname, float(self.resolution))
6638
- else:
6639
- local_res = None
6784
+ try:
6785
+ res_val = float(self.resolution) if self.resolution is not None else None
6786
+ except (TypeError, ValueError):
6787
+ res_val = None
6788
+ if res_val is not None:
6789
+ local_res = localres_histogram(self.hmodd.fullname, self.mapname, res_val)
6790
+ else:
6791
+ local_res = None
6640
6792
 
6641
6793
  if local_res:
6642
6794
  result_dict['local_res_histogram'] = local_res
@@ -18,5 +18,5 @@ under the License.
18
18
 
19
19
  """
20
20
 
21
- __version__ = '0.0.1.dev128'
21
+ __version__ = '0.0.1.dev130'
22
22
  __em_statistics_version__ = '202505.v01'
File without changes
File without changes
File without changes
File without changes
File without changes