emdbva 0.0.1.dev147__tar.gz → 0.0.1.dev148__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 (56) hide show
  1. {emdbva-0.0.1.dev147/emdbva.egg-info → emdbva-0.0.1.dev148}/PKG-INFO +1 -1
  2. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148/emdbva.egg-info}/PKG-INFO +1 -1
  3. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/emdbva.egg-info/SOURCES.txt +1 -0
  4. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/mainva.py +4 -0
  5. emdbva-0.0.1.dev148/va/metrics/map_map_cc.py +139 -0
  6. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/metrics/phaserandomization.py +3 -3
  7. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/preparation.py +2 -2
  8. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/prepareandrun_codon_airflow.py +0 -0
  9. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/utils/stars.py +536 -34
  10. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/validationanalysis.py +39 -2
  11. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/version.py +1 -1
  12. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/LICENSE +0 -0
  13. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/MANIFEST.in +0 -0
  14. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/README.rst +0 -0
  15. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/emdbva.egg-info/dependency_links.txt +0 -0
  16. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/emdbva.egg-info/entry_points.txt +0 -0
  17. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/emdbva.egg-info/requires.txt +0 -0
  18. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/emdbva.egg-info/top_level.txt +0 -0
  19. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/setup.cfg +0 -0
  20. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/setup.py +0 -0
  21. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/PATHS_codon_airflow.py +0 -0
  22. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/__init__.py +0 -0
  23. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/audit/__init__.py +0 -0
  24. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/audit/cif_revision.py +0 -0
  25. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/metrics/__init__.py +0 -0
  26. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/metrics/bars.py +0 -0
  27. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/metrics/connected_percentage.py +0 -0
  28. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/metrics/contour_level_predicator.py +0 -0
  29. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/metrics/emda_mmcc.py +0 -0
  30. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/metrics/emringer.py +0 -0
  31. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/metrics/inclusion.py +0 -0
  32. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/metrics/map_data_validation.py +0 -0
  33. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/metrics/overlap_percentage.py +0 -0
  34. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/metrics/phenix_cc.py +0 -0
  35. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/metrics/phenix_mm.py +0 -0
  36. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/metrics/projections.py +0 -0
  37. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/metrics/qscore.py +0 -0
  38. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/metrics/residue_locres.py +0 -0
  39. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/metrics/resmap.py +0 -0
  40. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/metrics/smoc.py +0 -0
  41. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/metrics/strudel.py +0 -0
  42. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/metrics/surfaces.py +0 -0
  43. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/metrics/threedfsc.py +0 -0
  44. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/qscores.csv +0 -0
  45. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/schemas/__init__.py +0 -0
  46. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/schemas/emdb_entry_full.schema.json +0 -0
  47. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/schemas/loader.py +0 -0
  48. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/utils/Checker.py +0 -0
  49. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/utils/ChimeraxViews.py +0 -0
  50. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/utils/MapProcessor.py +0 -0
  51. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/utils/Model.py +0 -0
  52. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/utils/__init__.py +0 -0
  53. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/utils/cl_weights.pth +0 -0
  54. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/utils/log_utils.py +0 -0
  55. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/va/utils/misc.py +0 -0
  56. {emdbva-0.0.1.dev147 → emdbva-0.0.1.dev148}/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.dev147
3
+ Version: 0.0.1.dev148
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.dev147
3
+ Version: 0.0.1.dev148
4
4
  Summary: CryoEM validation toolkit
5
5
  Home-page: https://test.pypi.org/project/va/
6
6
  Author: Zhe Wang
@@ -26,6 +26,7 @@ va/metrics/emda_mmcc.py
26
26
  va/metrics/emringer.py
27
27
  va/metrics/inclusion.py
28
28
  va/metrics/map_data_validation.py
29
+ va/metrics/map_map_cc.py
29
30
  va/metrics/overlap_percentage.py
30
31
  va/metrics/phaserandomization.py
31
32
  va/metrics/phenix_cc.py
@@ -119,6 +119,10 @@ def allruns(validationobj, runs):
119
119
  validationobj.fscs()
120
120
  # validationobj.surface_ratios()
121
121
 
122
+ # PHENIX map-map correlation
123
+ if 'mapmapcc' in runs:
124
+ validationobj.map_map_cc()
125
+
122
126
  # mmFSC
123
127
  if 'mmfsc' in runs:
124
128
  validationobj.phenix_mmfsc()
@@ -0,0 +1,139 @@
1
+ """PHENIX map-map correlation metric helpers."""
2
+
3
+ import os
4
+ import re
5
+ import subprocess
6
+ from distutils.spawn import find_executable
7
+
8
+
9
+ _MAP_MAP_CC_PATTERN = re.compile(
10
+ r"Map-map CC is\s+([-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?)"
11
+ )
12
+
13
+
14
+ def parse_map_map_cc_output(stdout_text):
15
+ """Extract the map-map correlation value from PHENIX output.
16
+
17
+ Args:
18
+ stdout_text (str): Combined standard output and error text from
19
+ ``phenix.map_correlations``.
20
+
21
+ Returns:
22
+ float or None: The parsed correlation, or ``None`` when the expected
23
+ result line is absent or cannot be converted to a number.
24
+ """
25
+ if not isinstance(stdout_text, str):
26
+ return None
27
+ match = _MAP_MAP_CC_PATTERN.search(stdout_text)
28
+ if match is None:
29
+ return None
30
+ try:
31
+ return float(match.group(1))
32
+ except (TypeError, ValueError):
33
+ return None
34
+
35
+
36
+ def run_map_map_cc(map1_path, map2_path, output_path, echo=True):
37
+ """Run PHENIX map-map correlation for two map files.
38
+
39
+ Args:
40
+ map1_path (str): Path to the first map.
41
+ map2_path (str): Path to the second map.
42
+ output_path (str): Directory for the PHENIX stdout diagnostic file.
43
+ echo (bool): Whether to print captured PHENIX output.
44
+
45
+ Returns:
46
+ tuple: ``(errlist, result, stdout_text)``. ``result`` is empty when
47
+ the executable, inputs, subprocess, or parser fails.
48
+ """
49
+ errlist = []
50
+ result = {}
51
+ stdout_text = ""
52
+
53
+ phenix_path = find_executable("phenix.map_correlations")
54
+ if phenix_path is None:
55
+ return ["Phenix executable phenix.map_correlations is not available."], result, stdout_text
56
+
57
+ if not os.path.isfile(map1_path):
58
+ errlist.append("Map file does not exist: {}".format(map1_path))
59
+ if not os.path.isfile(map2_path):
60
+ errlist.append("Map file does not exist: {}".format(map2_path))
61
+ if errlist:
62
+ return errlist, result, stdout_text
63
+
64
+ try:
65
+ os.makedirs(output_path, exist_ok=True)
66
+ except OSError as exc:
67
+ return ["Could not create output directory {}: {}".format(output_path, exc)], result, stdout_text
68
+
69
+ command = [phenix_path, map1_path, map2_path]
70
+ try:
71
+ completed = subprocess.run(
72
+ command,
73
+ cwd=output_path,
74
+ stdout=subprocess.PIPE,
75
+ stderr=subprocess.STDOUT,
76
+ text=True,
77
+ check=False,
78
+ )
79
+ stdout_text = completed.stdout or ""
80
+ except OSError as exc:
81
+ return ["Failed to run phenix.map_correlations: {}".format(exc)], result, stdout_text
82
+ finally:
83
+ log_path = os.path.join(output_path, "map_correlations.stdout")
84
+ try:
85
+ with open(log_path, "w", encoding="utf-8") as log_file:
86
+ log_file.write(stdout_text)
87
+ except OSError as exc:
88
+ errlist.append("Could not write {}: {}".format(log_path, exc))
89
+
90
+ if echo:
91
+ print(stdout_text, end="")
92
+ if completed.returncode != 0:
93
+ errlist.append("phenix.map_correlations exited with code {}".format(completed.returncode))
94
+ if "error" in stdout_text.lower():
95
+ errlist.extend(
96
+ line.strip() for line in stdout_text.splitlines() if "error" in line.lower()
97
+ )
98
+
99
+ correlation = parse_map_map_cc_output(stdout_text)
100
+ if correlation is None:
101
+ errlist.append("Could not parse 'Map-map CC is ...' from PHENIX output.")
102
+
103
+ if not errlist:
104
+ map1_name = os.path.basename(map1_path)
105
+ map2_name = os.path.basename(map2_path)
106
+ result = {"name": [map1_name, map2_name], "value": correlation}
107
+ return errlist, result, stdout_text
108
+
109
+
110
+ def write_map_map_cc_json(map1_path, map2_path, correlation, output_path):
111
+ """Write a successful map-map correlation result as a JSON fragment.
112
+
113
+ Args:
114
+ map1_path (str): Path to the first map.
115
+ map2_path (str): Path to the second map.
116
+ correlation (float): Parsed map-map correlation value.
117
+ output_path (str): Directory in which to write the fragment.
118
+
119
+ Returns:
120
+ str: Path to the written JSON file.
121
+ """
122
+ import json
123
+
124
+ os.makedirs(output_path, exist_ok=True)
125
+ map1_name = os.path.basename(map1_path)
126
+ map2_name = os.path.basename(map2_path)
127
+ pair_name = "{}_{}".format(map1_name, map2_name)
128
+ output_file = os.path.join(output_path, pair_name + "_cc.json")
129
+ data = {
130
+ "map_map_cc": {
131
+ pair_name: {
132
+ "names": [map1_name, map2_name],
133
+ "value": correlation,
134
+ }
135
+ }
136
+ }
137
+ with open(output_file, "w", encoding="utf-8") as handle:
138
+ json.dump(data, handle, indent=2)
139
+ return output_file
@@ -89,7 +89,7 @@ def relion_mask(raw_map, out_dir, mapname=None):
89
89
  print(f'Relion mask command: {relion_mask_cmd}')
90
90
  # Produce my own mask to compare with relion mask and time
91
91
  try:
92
- ownmask_loose = os.path.join(out_dir, f'{mapname}_ownmask.mrc')
92
+ ownmask_loose = os.path.join(out_dir, f'{mapname}_mask.mrc')
93
93
  relion_auto_mask(original_input_mrc, ownmask_loose, float(max_val_thirty), dilatepx, softpx)
94
94
  print(f'Relion auto mask command: {relion_auto_mask.__name__}')
95
95
  except Exception as e:
@@ -97,7 +97,7 @@ def relion_mask(raw_map, out_dir, mapname=None):
97
97
  ownmask_loose = None
98
98
 
99
99
  if angpix and max_val_thirty and dilatepx and softpx and input_mrc:
100
- subprocess.run(relion_mask_cmd, shell=True)
100
+ # subprocess.run(relion_mask_cmd, shell=True)
101
101
  if not MapProcessor.check_map_starts(mask_loose, original_input_mrc):
102
102
  print('Relion mask does not have the same nstarts as the original map.')
103
103
  MapProcessor.update_map_starts(original_input_mrc, mask_loose)
@@ -221,7 +221,7 @@ def relion_auto_mask_fast(data, threshold, extend_pixels, edge_width, verbose=Tr
221
221
  steps = []
222
222
  if verbose:
223
223
  steps = tqdm(total=3, desc="Masking Steps", unit="step")
224
-
224
+
225
225
  # Step 1: Initial binary mask
226
226
  mask = (data >= threshold).astype(np.float32)
227
227
  if verbose:
@@ -910,7 +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
- 'phrand': False, 'predictcontour': False}
913
+ 'phrand': False, 'predictcontour': False, 'mapmapcc': False}
914
914
  for key in resdict.keys():
915
915
  if key in runs:
916
916
  resdict[key] = True
@@ -924,7 +924,7 @@ class PreParation:
924
924
  if self.mapname is not None:
925
925
  runlist.extend(['projection', 'central', 'surface', 'volume', 'density', 'raps', 'largestvariance',
926
926
  'mask', 'fsc', 'mmfsc', 'rmmcc', 'symmetry', 'qscore', 'strudel', 'emringer', '3dfsc',
927
- 'smoc', 'resccc', 'locres', 'phrand', 'predictcontour',
927
+ 'smoc', 'resccc', 'locres', 'phrand', 'predictcontour', 'mapmapcc',
928
928
  ])
929
929
 
930
930
  if self.masks is None:
@@ -757,9 +757,139 @@ class GetStars:
757
757
 
758
758
  return area
759
759
 
760
+ @staticmethod
761
+ def curve_touches_or_crosses_zero(curve, atol=1e-12):
762
+ """
763
+ Check whether a curve touches or crosses y=0.
764
+
765
+ Returns
766
+ -------
767
+ bool
768
+ True if:
769
+ - any sampled value is approximately zero, OR
770
+ - two adjacent values have opposite signs.
771
+
772
+ False if the entire curve stays strictly on one side of zero.
773
+
774
+ Parameters
775
+ ----------
776
+ curve : array-like
777
+ Y values of the curve.
778
+
779
+ atol : float
780
+ Absolute tolerance used to consider a value equal to zero.
781
+ """
782
+
783
+ curve = np.asarray(curve, dtype=float)
784
+
785
+ # Ignore NaN / +/-inf values.
786
+ curve = curve[np.isfinite(curve)]
787
+
788
+ if curve.size == 0:
789
+ return False
790
+
791
+ # Does an actual sampled point touch zero?
792
+ if np.any(np.isclose(curve, 0.0, atol=atol, rtol=0.0)):
793
+ return True
794
+
795
+ # Does the curve cross zero between two adjacent samples?
796
+ crosses_zero = np.any(
797
+ curve[:-1] * curve[1:] < 0
798
+ )
799
+
800
+ return bool(crosses_zero)
801
+
760
802
  def feature_zone(self, data_curves):
761
803
  """
762
- Given two curves, calculate feature zone and overfit zone.
804
+ Calculate feature-zone, overfit-zone, and masking diagnostics.
805
+
806
+ Masking notation
807
+ ----------------
808
+ M = masked FSC
809
+ C = corrected FSC
810
+ U = unmasked FSC
811
+
812
+ The masking metrics are:
813
+
814
+ N = integral(M - C) dx
815
+
816
+ Signed Masked-Corrected area.
817
+ N > 0 : M is above C in the integrated sense.
818
+ N < 0 : C is above M in the integrated sense.
819
+
820
+ D = integral(M - U) dx
821
+
822
+ Signed Masked-Unmasked area.
823
+ D > 0 : M is above U in the integrated sense.
824
+ D < 0 : U is above M in the integrated sense.
825
+
826
+ R_signed = N / D
827
+
828
+ Signed relative position of C with respect to M and U.
829
+ This is useful for determining the integrated ordering
830
+ of the three curves, but can become unreliable when D
831
+ is close to zero because of curve crossing/cancellation.
832
+
833
+ MC_abs = integral(|M - C|) dx
834
+
835
+ Total absolute separation between Masked and Corrected.
836
+
837
+ MU_abs = integral(|M - U|) dx
838
+
839
+ Total absolute separation between Masked and Unmasked.
840
+
841
+ R_abs = MC_abs / MU_abs
842
+
843
+ Main masking discrepancy metric.
844
+
845
+ This value is stored using the existing key:
846
+
847
+ masking_area_ratio
848
+
849
+ for backwards compatibility.
850
+
851
+ R_abs ~= 0:
852
+ Masked and Corrected track very closely.
853
+
854
+ Increasing R_abs:
855
+ Increasing Masked-Corrected disagreement relative
856
+ to the overall Masked-Unmasked separation.
857
+
858
+ R_abs ~= 1:
859
+ Masked-Corrected disagreement is approximately as
860
+ large as Masked-Unmasked disagreement.
861
+
862
+ R_abs > 1:
863
+ Corrected is, in total absolute area, farther from
864
+ Masked than Unmasked is from Masked.
865
+
866
+ Q_mc = |N| / MC_abs
867
+
868
+ Directionality/cancellation metric for M-C.
869
+
870
+ Q_mc ~= 1:
871
+ M-C has mostly one sign. The sign of N is meaningful.
872
+
873
+ Q_mc ~= 0:
874
+ Strong crossing/cancellation between M and C.
875
+ The sign of N alone should not be interpreted as a
876
+ simple global curve ordering.
877
+
878
+ Q_mu = |D| / MU_abs
879
+
880
+ Directionality/cancellation metric for M-U.
881
+
882
+ Q_mu ~= 1:
883
+ M-U has mostly one sign. The sign of D is meaningful.
884
+
885
+ Q_mu ~= 0:
886
+ Strong crossing/cancellation between M and U.
887
+ R_signed may therefore be unstable or misleading.
888
+
889
+ Notes
890
+ -----
891
+ All masking metrics are calculated over the same complete valid
892
+ frequency range. No high-frequency-only restriction is applied.
763
893
  """
764
894
 
765
895
  zones = {}
@@ -771,28 +901,42 @@ class GetStars:
771
901
 
772
902
  levels = all_curves.get('level')
773
903
  corrected_curve = all_curves.get('fsc_corrected')
774
-
775
904
  phase_rand_curve = all_curves.get('phaserandomization')
905
+
776
906
  if phase_rand_curve is None:
777
907
  phase_rand_curve = all_curves.get('phaserandmization')
778
908
 
779
909
  masked_curve = all_curves.get('fsc_masked')
780
910
  unmasked_curve = all_curves.get('fsc')
781
911
 
782
- if levels is None or corrected_curve is None or phase_rand_curve is None:
912
+ if (
913
+ levels is None
914
+ or corrected_curve is None
915
+ or phase_rand_curve is None
916
+ ):
783
917
  return result
784
918
 
785
919
  try:
920
+ # ----------------------------------------------------------
921
+ # Prepare feature-zone curves
922
+ # ----------------------------------------------------------
923
+
786
924
  levels = np.asarray(levels, dtype=float)
787
925
  corrected_curve = np.asarray(corrected_curve, dtype=float)
788
926
  phase_rand_curve = np.asarray(phase_rand_curve, dtype=float)
789
927
 
790
- min_len = min(len(levels), len(corrected_curve), len(phase_rand_curve))
928
+ min_len = min(
929
+ len(levels),
930
+ len(corrected_curve),
931
+ len(phase_rand_curve),
932
+ )
791
933
 
792
934
  levels = levels[:min_len]
793
935
  corrected_curve = corrected_curve[:min_len]
794
936
  phase_rand_curve = phase_rand_curve[:min_len]
795
937
 
938
+ # Remove points that cannot be used for the feature-zone
939
+ # calculation.
796
940
  valid = (
797
941
  np.isfinite(levels)
798
942
  & np.isfinite(corrected_curve)
@@ -806,11 +950,38 @@ class GetStars:
806
950
  if len(levels) < 2:
807
951
  return result
808
952
 
953
+ # Integration and interpolation require an ordered x axis.
809
954
  order = np.argsort(levels)
955
+
810
956
  levels = levels[order]
811
957
  corrected_curve = corrected_curve[order]
812
958
  phase_rand_curve = phase_rand_curve[order]
813
959
 
960
+ # ----------------------------------------------------------
961
+ # Check whether corrected FSC and phase-randomized FSC
962
+ # touch or cross y=0.
963
+ # ----------------------------------------------------------
964
+
965
+ corrected_touches_zero = self.curve_touches_or_crosses_zero(
966
+ corrected_curve
967
+ )
968
+
969
+ phase_touches_zero = self.curve_touches_or_crosses_zero(
970
+ phase_rand_curve
971
+ )
972
+
973
+ feature_zone_corrected_fsc_nottouching_zero = (
974
+ not corrected_touches_zero
975
+ )
976
+
977
+ feature_zone_phase_fsc_nottouching_zero = (
978
+ not phase_touches_zero
979
+ )
980
+
981
+ # ----------------------------------------------------------
982
+ # Feature-zone / overfit-zone calculation
983
+ # ----------------------------------------------------------
984
+
814
985
  xs, ys = interpolated_intercepts_general(
815
986
  levels,
816
987
  corrected_curve,
@@ -819,7 +990,11 @@ class GetStars:
819
990
 
820
991
  intersections = remove_duplicate_intersections(xs, ys)
821
992
 
822
- nlevels, ncorrected_curve, nphase_rand_curve = self.intersections_into_curve(
993
+ (
994
+ nlevels,
995
+ ncorrected_curve,
996
+ nphase_rand_curve,
997
+ ) = self.intersections_into_curve(
823
998
  intersections,
824
999
  levels,
825
1000
  corrected_curve,
@@ -833,63 +1008,390 @@ class GetStars:
833
1008
  nphase_rand_curve,
834
1009
  )
835
1010
 
1011
+ # ----------------------------------------------------------
1012
+ # Default masking values
1013
+ # ----------------------------------------------------------
1014
+
1015
+ masking_area = 0.0
1016
+
1017
+ # Existing indexed field.
1018
+ # This will now contain R_abs.
1019
+ masking_area_ratio = np.nan
1020
+
1021
+ masked_corrected_signed_area = np.nan # N
1022
+ masked_unmasked_signed_area = np.nan # D
1023
+
1024
+ masked_corrected_absolute_area = np.nan # MC_abs
1025
+ masked_unmasked_absolute_area = np.nan # MU_abs
1026
+
1027
+ masking_signed_ratio = np.nan # R_signed
1028
+
1029
+ masked_corrected_directionality = np.nan # Q_mc
1030
+ masked_unmasked_directionality = np.nan # Q_mu
1031
+
1032
+ # ----------------------------------------------------------
1033
+ # Masking calculations
1034
+ # ----------------------------------------------------------
1035
+
836
1036
  if masked_curve is not None and unmasked_curve is not None:
837
- masked_curve = np.asarray(masked_curve, dtype=float)[:min_len][valid]
838
- unmasked_curve = np.asarray(unmasked_curve, dtype=float)[:min_len][valid]
839
1037
 
1038
+ # Apply exactly the same initial trimming and validity mask
1039
+ # that was used for levels/corrected/phase-randomized curves.
1040
+ masked_curve = np.asarray(
1041
+ masked_curve,
1042
+ dtype=float,
1043
+ )[:min_len][valid]
1044
+
1045
+ unmasked_curve = np.asarray(
1046
+ unmasked_curve,
1047
+ dtype=float,
1048
+ )[:min_len][valid]
1049
+
1050
+ # Apply the same sorting order.
840
1051
  masked_curve = masked_curve[order]
841
1052
  unmasked_curve = unmasked_curve[order]
842
1053
 
843
- masking_area = self.area_difference(
844
- masked_curve,
845
- corrected_curve,
846
- levels,
1054
+ # ------------------------------------------------------
1055
+ # There may still be non-finite values in M or U even
1056
+ # though level/C/phase-randomized were valid.
1057
+ #
1058
+ # Use one common mask so M, C, U and x always remain
1059
+ # aligned.
1060
+ # ------------------------------------------------------
1061
+
1062
+ mask_valid = (
1063
+ np.isfinite(levels)
1064
+ & np.isfinite(masked_curve)
1065
+ & np.isfinite(corrected_curve)
1066
+ & np.isfinite(unmasked_curve)
847
1067
  )
848
1068
 
849
- corrected_unmasked_difference = np.nansum(
850
- corrected_curve - unmasked_curve
851
- )
852
- corrected_masked_difference = np.nansum(
853
- masked_curve - corrected_curve
854
- )
1069
+ mask_levels = levels[mask_valid]
1070
+ mask_masked = masked_curve[mask_valid]
1071
+ mask_corrected = corrected_curve[mask_valid]
1072
+ mask_unmasked = unmasked_curve[mask_valid]
1073
+
1074
+ if len(mask_levels) >= 2:
1075
+
1076
+ # ==================================================
1077
+ # Signed areas
1078
+ # ==================================================
1079
+
1080
+ # N = integral(M - C) dx
1081
+ #
1082
+ # Positive:
1083
+ # Masked has more integrated area than Corrected.
1084
+ #
1085
+ # Negative:
1086
+ # Corrected has more integrated area than Masked.
1087
+ masked_corrected_signed_area = np.trapz(
1088
+ mask_masked - mask_corrected,
1089
+ mask_levels,
1090
+ )
855
1091
 
856
- if corrected_unmasked_difference == 0:
857
- masking_area_ratio = 999
858
- else:
859
- masking_area_ratio = abs(
860
- corrected_masked_difference / corrected_unmasked_difference
1092
+ # D = integral(M - U) dx
1093
+ #
1094
+ # Positive:
1095
+ # Masked has more integrated area than Unmasked.
1096
+ #
1097
+ # Negative:
1098
+ # Unmasked has more integrated area than Masked.
1099
+ masked_unmasked_signed_area = np.trapz(
1100
+ mask_masked - mask_unmasked,
1101
+ mask_levels,
1102
+ )
1103
+
1104
+ # ==================================================
1105
+ # Absolute areas
1106
+ # ==================================================
1107
+
1108
+ # Total geometrical separation between M and C.
1109
+ masked_corrected_absolute_area = np.trapz(
1110
+ np.abs(mask_masked - mask_corrected),
1111
+ mask_levels,
1112
+ )
1113
+
1114
+ # Total geometrical separation between M and U.
1115
+ masked_unmasked_absolute_area = np.trapz(
1116
+ np.abs(mask_masked - mask_unmasked),
1117
+ mask_levels,
1118
+ )
1119
+
1120
+ # Keep the historical "masking_area" field, but make
1121
+ # its definition consistent with the new masking
1122
+ # metric:
1123
+ #
1124
+ # masking_area = integral(|M-C|) dx
1125
+ #
1126
+ masking_area = masked_corrected_absolute_area
1127
+
1128
+ # ==================================================
1129
+ # R_signed = N / D
1130
+ # ==================================================
1131
+
1132
+ if not np.isclose(
1133
+ masked_unmasked_signed_area,
1134
+ 0.0,
1135
+ rtol=1e-8,
1136
+ atol=1e-12,
1137
+ ):
1138
+ masking_signed_ratio = (
1139
+ masked_corrected_signed_area
1140
+ / masked_unmasked_signed_area
1141
+ )
1142
+ else:
1143
+ # D ~= 0 makes N/D undefined or numerically
1144
+ # unstable.
1145
+ masking_signed_ratio = np.nan
1146
+
1147
+ # ==================================================
1148
+ # R_abs = MC_abs / MU_abs
1149
+ #
1150
+ # THIS replaces the old masking_area_ratio.
1151
+ # ==================================================
1152
+
1153
+ if not np.isclose(
1154
+ masked_unmasked_absolute_area,
1155
+ 0.0,
1156
+ rtol=1e-8,
1157
+ atol=1e-12,
1158
+ ):
1159
+ masking_area_ratio = (
1160
+ masked_corrected_absolute_area
1161
+ / masked_unmasked_absolute_area
1162
+ )
1163
+ else:
1164
+ # If M and U have essentially no absolute
1165
+ # separation, there is no meaningful denominator
1166
+ # against which to normalize M-C.
1167
+ masking_area_ratio = np.nan
1168
+
1169
+ # ==================================================
1170
+ # Q_mc
1171
+ #
1172
+ # |integral(M-C)| / integral(|M-C|)
1173
+ # ==================================================
1174
+
1175
+ if not np.isclose(
1176
+ masked_corrected_absolute_area,
1177
+ 0.0,
1178
+ rtol=1e-8,
1179
+ atol=1e-12,
1180
+ ):
1181
+ masked_corrected_directionality = (
1182
+ abs(masked_corrected_signed_area)
1183
+ / masked_corrected_absolute_area
1184
+ )
1185
+ else:
1186
+ # M and C essentially coincide.
1187
+ #
1188
+ # There is no cancellation to diagnose. Setting
1189
+ # Q_mc to 1 is convenient because their difference
1190
+ # is perfectly consistent (zero everywhere within
1191
+ # numerical tolerance).
1192
+ masked_corrected_directionality = 1.0
1193
+
1194
+ # ==================================================
1195
+ # Q_mu
1196
+ #
1197
+ # |integral(M-U)| / integral(|M-U|)
1198
+ # ==================================================
1199
+
1200
+ if not np.isclose(
1201
+ masked_unmasked_absolute_area,
1202
+ 0.0,
1203
+ rtol=1e-8,
1204
+ atol=1e-12,
1205
+ ):
1206
+ masked_unmasked_directionality = (
1207
+ abs(masked_unmasked_signed_area)
1208
+ / masked_unmasked_absolute_area
1209
+ )
1210
+ else:
1211
+ # Same convention as Q_mc.
1212
+ masked_unmasked_directionality = 1.0
1213
+
1214
+ # Numerical integration may occasionally produce
1215
+ # 1.0000000000000002 etc.
1216
+ masked_corrected_directionality = min(
1217
+ 1.0,
1218
+ max(
1219
+ 0.0,
1220
+ float(masked_corrected_directionality),
1221
+ ),
1222
+ )
1223
+
1224
+ masked_unmasked_directionality = min(
1225
+ 1.0,
1226
+ max(
1227
+ 0.0,
1228
+ float(masked_unmasked_directionality),
1229
+ ),
861
1230
  )
862
- else:
863
- masking_area = 0.0
864
- masking_area_ratio = 0.0
1231
+
1232
+ # ----------------------------------------------------------
1233
+ # Convert main feature-zone results
1234
+ # ----------------------------------------------------------
865
1235
 
866
1236
  feature_area = float(feature_area or 0.0)
867
1237
  overfit_area = float(overfit_area or 0.0)
868
1238
  masking_area = float(masking_area or 0.0)
869
- masking_area_ratio = float(masking_area_ratio or 0.0)
870
1239
 
871
- zones['feature_zone'] = keep_three_significant_digits(feature_area)
872
- zones['overfit_zone'] = keep_three_significant_digits(overfit_area)
873
- zones['masking_area'] = keep_three_significant_digits(masking_area)
874
- zones['masking_area_ratio'] = keep_three_significant_digits(masking_area_ratio)
1240
+ # ----------------------------------------------------------
1241
+ # Existing output fields
1242
+ # ----------------------------------------------------------
1243
+
1244
+ zones['feature_zone'] = keep_three_significant_digits(
1245
+ feature_area
1246
+ )
1247
+
1248
+ zones['overfit_zone'] = keep_three_significant_digits(
1249
+ overfit_area
1250
+ )
1251
+
1252
+ zones['masking_area'] = keep_three_significant_digits(
1253
+ masking_area
1254
+ )
1255
+
1256
+ zones['feature_zone_corrected_fsc_nottouching_zero'] = (
1257
+ feature_zone_corrected_fsc_nottouching_zero
1258
+ )
1259
+
1260
+ zones['feature_zone_phase_fsc_nottouching_zero'] = (
1261
+ feature_zone_phase_fsc_nottouching_zero
1262
+ )
1263
+
1264
+ # IMPORTANT:
1265
+ #
1266
+ # Keep this key unchanged because it is already indexed by
1267
+ # downstream code.
1268
+ #
1269
+ # It now represents:
1270
+ #
1271
+ # integral(|M-C|) / integral(|M-U|)
1272
+ #
1273
+ if np.isfinite(masking_area_ratio):
1274
+ zones['masking_area_ratio'] = keep_three_significant_digits(
1275
+ float(masking_area_ratio)
1276
+ )
1277
+ else:
1278
+ zones['masking_area_ratio'] = None
1279
+
1280
+ # ----------------------------------------------------------
1281
+ # New masking diagnostic fields
1282
+ # ----------------------------------------------------------
1283
+
1284
+ # N
1285
+ if np.isfinite(masked_corrected_signed_area):
1286
+ zones['masked_corrected_signed_area'] = (
1287
+ keep_three_significant_digits(
1288
+ float(masked_corrected_signed_area)
1289
+ )
1290
+ )
1291
+ else:
1292
+ zones['masked_corrected_signed_area'] = None
1293
+
1294
+ # D
1295
+ if np.isfinite(masked_unmasked_signed_area):
1296
+ zones['masked_unmasked_signed_area'] = (
1297
+ keep_three_significant_digits(
1298
+ float(masked_unmasked_signed_area)
1299
+ )
1300
+ )
1301
+ else:
1302
+ zones['masked_unmasked_signed_area'] = None
1303
+
1304
+ # Absolute M-C area
1305
+ if np.isfinite(masked_corrected_absolute_area):
1306
+ zones['masked_corrected_absolute_area'] = (
1307
+ keep_three_significant_digits(
1308
+ float(masked_corrected_absolute_area)
1309
+ )
1310
+ )
1311
+ else:
1312
+ zones['masked_corrected_absolute_area'] = None
1313
+
1314
+ # Absolute M-U area
1315
+ if np.isfinite(masked_unmasked_absolute_area):
1316
+ zones['masked_unmasked_absolute_area'] = (
1317
+ keep_three_significant_digits(
1318
+ float(masked_unmasked_absolute_area)
1319
+ )
1320
+ )
1321
+ else:
1322
+ zones['masked_unmasked_absolute_area'] = None
1323
+
1324
+ # R_signed
1325
+ if np.isfinite(masking_signed_ratio):
1326
+ zones['masking_signed_ratio'] = (
1327
+ keep_three_significant_digits(
1328
+ float(masking_signed_ratio)
1329
+ )
1330
+ )
1331
+ else:
1332
+ zones['masking_signed_ratio'] = None
1333
+
1334
+ # Q_mc
1335
+ if np.isfinite(masked_corrected_directionality):
1336
+ zones['masked_corrected_directionality'] = (
1337
+ keep_three_significant_digits(
1338
+ float(masked_corrected_directionality)
1339
+ )
1340
+ )
1341
+ else:
1342
+ zones['masked_corrected_directionality'] = None
1343
+
1344
+ # Q_mu
1345
+ if np.isfinite(masked_unmasked_directionality):
1346
+ zones['masked_unmasked_directionality'] = (
1347
+ keep_three_significant_digits(
1348
+ float(masked_unmasked_directionality)
1349
+ )
1350
+ )
1351
+ else:
1352
+ zones['masked_unmasked_directionality'] = None
1353
+
1354
+ # ----------------------------------------------------------
1355
+ # Feature-zone ratio
1356
+ # ----------------------------------------------------------
875
1357
 
876
1358
  total_area = feature_area + overfit_area
877
- if total_area == 0:
1359
+
1360
+ if np.isclose(total_area, 0.0):
878
1361
  zones['feature_zone_ratio'] = 0
879
1362
  else:
880
- zones['feature_zone_ratio'] = keep_three_significant_digits(
881
- feature_area / total_area
1363
+ zones['feature_zone_ratio'] = (
1364
+ keep_three_significant_digits(
1365
+ feature_area / total_area
1366
+ )
882
1367
  )
883
1368
 
884
1369
  result['feature_zones'] = zones
885
1370
 
886
1371
  except Exception as exc:
887
1372
  print(f'Feature zone calculation failed: {exc}')
1373
+
888
1374
  result['feature_zones'] = {
889
1375
  'feature_zone': 0,
890
1376
  'overfit_zone': 0,
1377
+
1378
+ 'feature_zone_corrected_fsc_nottouching_zero': None,
1379
+ 'feature_zone_phase_fsc_nottouching_zero': None,
1380
+
1381
+ # Existing indexed fields
891
1382
  'masking_area': 0,
892
- 'masking_area_ratio': 0,
1383
+ 'masking_area_ratio': None,
1384
+
1385
+
1386
+ # New masking diagnostics
1387
+ 'masked_corrected_signed_area': None,
1388
+ 'masked_unmasked_signed_area': None,
1389
+ 'masked_corrected_absolute_area': None,
1390
+ 'masked_unmasked_absolute_area': None,
1391
+ 'masking_signed_ratio': None,
1392
+ 'masked_corrected_directionality': None,
1393
+ 'masked_unmasked_directionality': None,
1394
+
893
1395
  'feature_zone_ratio': 0,
894
1396
  'error': str(exc),
895
1397
  }
@@ -77,6 +77,7 @@ from va.metrics.qscore import *
77
77
  from va.metrics.inclusion import *
78
78
  from va.metrics.connected_percentage import *
79
79
  from va.metrics.overlap_percentage import *
80
+ from va.metrics.map_map_cc import run_map_map_cc, write_map_map_cc_json
80
81
  from metrics.map_data_validation import run_map_data_validation
81
82
  import va
82
83
 
@@ -227,8 +228,8 @@ class ValidationAnalysis:
227
228
  self.get_resolution()
228
229
  self.relion_mask = None
229
230
  self.masked_rawmap = None
230
- if self.rawmap:
231
- self.relion_mask = self.get_relion_mask()
231
+ # if self.rawmap:
232
+ # self.relion_mask = self.get_relion_mask()
232
233
  # make a symbolic link for relion_mask in the va folder
233
234
  if self.relion_mask:
234
235
  mask_name = os.path.basename(self.relion_mask)
@@ -4053,6 +4054,42 @@ class ValidationAnalysis:
4053
4054
  print('Mising half map(s).')
4054
4055
 
4055
4056
 
4057
+ def map_map_cc(self):
4058
+ """Calculate PHENIX map-map CC for the primary and half maps.
4059
+
4060
+ The metric is opt-in through the ``mapmapcc`` run token. It writes a
4061
+ JSON fragment for each successful primary/half-map or half-map pair
4062
+ and skips cleanly when the required maps are unavailable.
4063
+ """
4064
+ if self.map is None or self.hmeven is None or self.hmodd is None:
4065
+ print('Map-map CC skipped: primary map and both half maps are required.')
4066
+ return None
4067
+
4068
+ primary_path = getattr(self.map, 'fullname', None)
4069
+ even_path = getattr(self.hmeven, 'fullname', None)
4070
+ odd_path = getattr(self.hmodd, 'fullname', None)
4071
+ if not primary_path or not even_path or not odd_path:
4072
+ print('Map-map CC skipped: primary or half-map filenames are unavailable.')
4073
+ return None
4074
+ if not all(os.path.isfile(path) for path in (primary_path, even_path, odd_path)):
4075
+ print('Map-map CC skipped: one or more required map files do not exist.')
4076
+ return None
4077
+
4078
+ map_pairs = (
4079
+ (primary_path, odd_path),
4080
+ (primary_path, even_path),
4081
+ (even_path, odd_path),
4082
+ )
4083
+ for map1_path, map2_path in map_pairs:
4084
+ errlist, result, _stdout = run_map_map_cc(map1_path, map2_path, self.workdir)
4085
+ if errlist:
4086
+ for error in errlist:
4087
+ sys.stderr.write('Map-map CC error: {}\n'.format(error))
4088
+ continue
4089
+ write_map_map_cc_json(map1_path, map2_path, result['value'], self.workdir)
4090
+ return None
4091
+
4092
+
4056
4093
  def readfsc(self, asym=1.0):
4057
4094
  """
4058
4095
 
@@ -18,5 +18,5 @@ under the License.
18
18
 
19
19
  """
20
20
 
21
- __version__ = '0.0.1.dev147'
21
+ __version__ = '0.0.1.dev148'
22
22
  __em_statistics_version__ = '202505.v01'
File without changes
File without changes
File without changes
File without changes
File without changes