RNApolis 0.10.4__py3-none-any.whl → 0.10.6__py3-none-any.whl
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.
- rnapolis/adapter.py +384 -18
 - rnapolis/annotator.py +17 -110
 - rnapolis/common.py +98 -1
 - {rnapolis-0.10.4.dist-info → rnapolis-0.10.6.dist-info}/METADATA +1 -1
 - {rnapolis-0.10.4.dist-info → rnapolis-0.10.6.dist-info}/RECORD +9 -9
 - {rnapolis-0.10.4.dist-info → rnapolis-0.10.6.dist-info}/WHEEL +0 -0
 - {rnapolis-0.10.4.dist-info → rnapolis-0.10.6.dist-info}/entry_points.txt +0 -0
 - {rnapolis-0.10.4.dist-info → rnapolis-0.10.6.dist-info}/licenses/LICENSE +0 -0
 - {rnapolis-0.10.4.dist-info → rnapolis-0.10.6.dist-info}/top_level.txt +0 -0
 
    
        rnapolis/adapter.py
    CHANGED
    
    | 
         @@ -8,7 +8,7 @@ from collections import defaultdict 
     | 
|
| 
       8 
8 
     | 
    
         
             
            from dataclasses import dataclass
         
     | 
| 
       9 
9 
     | 
    
         
             
            from enum import Enum
         
     | 
| 
       10 
10 
     | 
    
         
             
            from tempfile import NamedTemporaryFile
         
     | 
| 
       11 
     | 
    
         
            -
            from typing import DefaultDict, Dict, List, Optional, Set, Tuple
         
     | 
| 
      
 11 
     | 
    
         
            +
            from typing import Any, DefaultDict, Dict, List, Optional, Set, Tuple, Union
         
     | 
| 
       12 
12 
     | 
    
         | 
| 
       13 
13 
     | 
    
         
             
            import orjson
         
     | 
| 
       14 
14 
     | 
    
         | 
| 
         @@ -49,6 +49,7 @@ class ExternalTool(Enum): 
     | 
|
| 
       49 
49 
     | 
    
         
             
                BPNET = "bpnet"
         
     | 
| 
       50 
50 
     | 
    
         
             
                MAXIT = "maxit"
         
     | 
| 
       51 
51 
     | 
    
         
             
                BARNABA = "barnaba"
         
     | 
| 
      
 52 
     | 
    
         
            +
                MCANNOTATE = "mc-annotate"
         
     | 
| 
       52 
53 
     | 
    
         | 
| 
       53 
54 
     | 
    
         | 
| 
       54 
55 
     | 
    
         
             
            logging.basicConfig(level=os.getenv("LOGLEVEL", "INFO").upper())
         
     | 
| 
         @@ -68,25 +69,30 @@ def auto_detect_tool(external_files: List[str]) -> ExternalTool: 
     | 
|
| 
       68 
69 
     | 
    
         
             
                    return ExternalTool.MAXIT
         
     | 
| 
       69 
70 
     | 
    
         | 
| 
       70 
71 
     | 
    
         
             
                for file_path in external_files:
         
     | 
| 
      
 72 
     | 
    
         
            +
                    basename = os.path.basename(file_path)
         
     | 
| 
      
 73 
     | 
    
         
            +
             
     | 
| 
       71 
74 
     | 
    
         
             
                    # Check for FR3D pattern
         
     | 
| 
       72 
     | 
    
         
            -
                    if  
     | 
| 
      
 75 
     | 
    
         
            +
                    if basename.endswith("basepair_detail.txt"):
         
     | 
| 
       73 
76 
     | 
    
         
             
                        return ExternalTool.FR3D
         
     | 
| 
       74 
77 
     | 
    
         | 
| 
       75 
78 
     | 
    
         
             
                    # Check for RNAView pattern
         
     | 
| 
       76 
     | 
    
         
            -
                    if  
     | 
| 
      
 79 
     | 
    
         
            +
                    if basename.endswith(".out"):
         
     | 
| 
       77 
80 
     | 
    
         
             
                        return ExternalTool.RNAVIEW
         
     | 
| 
       78 
81 
     | 
    
         | 
| 
       79 
82 
     | 
    
         
             
                    # Check for BPNet pattern
         
     | 
| 
       80 
     | 
    
         
            -
                    if  
     | 
| 
      
 83 
     | 
    
         
            +
                    if basename.endswith("basepair.json"):
         
     | 
| 
       81 
84 
     | 
    
         
             
                        return ExternalTool.BPNET
         
     | 
| 
       82 
85 
     | 
    
         | 
| 
      
 86 
     | 
    
         
            +
                    # Check for MC-Annotate pattern
         
     | 
| 
      
 87 
     | 
    
         
            +
                    if basename.endswith("stdout.txt"):
         
     | 
| 
      
 88 
     | 
    
         
            +
                        return ExternalTool.MCANNOTATE
         
     | 
| 
      
 89 
     | 
    
         
            +
             
     | 
| 
       83 
90 
     | 
    
         
             
                    # Check for Barnaba pattern
         
     | 
| 
       84 
     | 
    
         
            -
                    basename = os.path.basename(file_path)
         
     | 
| 
       85 
91 
     | 
    
         
             
                    if "pairing" in basename or "stacking" in basename:
         
     | 
| 
       86 
92 
     | 
    
         
             
                        return ExternalTool.BARNABA
         
     | 
| 
       87 
93 
     | 
    
         | 
| 
       88 
94 
     | 
    
         
             
                    # Check for JSON files (DSSR)
         
     | 
| 
       89 
     | 
    
         
            -
                    if  
     | 
| 
      
 95 
     | 
    
         
            +
                    if basename.endswith(".json"):
         
     | 
| 
       90 
96 
     | 
    
         
             
                        return ExternalTool.DSSR
         
     | 
| 
       91 
97 
     | 
    
         | 
| 
       92 
98 
     | 
    
         
             
                # Default to MAXIT if no patterns match
         
     | 
| 
         @@ -317,10 +323,14 @@ def parse_dssr_output( 
     | 
|
| 
       317 
323 
     | 
    
         
             
                        if nt1 is not None and nt2 is not None:
         
     | 
| 
       318 
324 
     | 
    
         
             
                            stackings.append(Stacking(nt1, nt2, None))
         
     | 
| 
       319 
325 
     | 
    
         | 
| 
       320 
     | 
    
         
            -
                return BaseInteractions( 
     | 
| 
      
 326 
     | 
    
         
            +
                return BaseInteractions.from_structure3d(
         
     | 
| 
      
 327 
     | 
    
         
            +
                    structure3d, base_pairs, stackings, [], [], []
         
     | 
| 
      
 328 
     | 
    
         
            +
                )
         
     | 
| 
       321 
329 
     | 
    
         | 
| 
       322 
330 
     | 
    
         | 
| 
       323 
     | 
    
         
            -
            def parse_maxit_output( 
     | 
| 
      
 331 
     | 
    
         
            +
            def parse_maxit_output(
         
     | 
| 
      
 332 
     | 
    
         
            +
                file_paths: List[str], structure3d: Structure3D
         
     | 
| 
      
 333 
     | 
    
         
            +
            ) -> BaseInteractions:
         
     | 
| 
       324 
334 
     | 
    
         
             
                """
         
     | 
| 
       325 
335 
     | 
    
         
             
                Parse MAXIT output files and convert to BaseInteractions.
         
     | 
| 
       326 
336 
     | 
    
         | 
| 
         @@ -448,10 +458,14 @@ def parse_maxit_output(file_paths: List[str]) -> BaseInteractions: 
     | 
|
| 
       448 
458 
     | 
    
         
             
                except Exception as e:
         
     | 
| 
       449 
459 
     | 
    
         
             
                    logging.warning(f"Error processing MAXIT file {cif_file}: {e}", exc_info=True)
         
     | 
| 
       450 
460 
     | 
    
         | 
| 
       451 
     | 
    
         
            -
                return BaseInteractions( 
     | 
| 
      
 461 
     | 
    
         
            +
                return BaseInteractions.from_structure3d(
         
     | 
| 
      
 462 
     | 
    
         
            +
                    structure3d, all_base_pairs, [], [], [], all_other_interactions
         
     | 
| 
      
 463 
     | 
    
         
            +
                )
         
     | 
| 
       452 
464 
     | 
    
         | 
| 
       453 
465 
     | 
    
         | 
| 
       454 
     | 
    
         
            -
            def parse_bpnet_output( 
     | 
| 
      
 466 
     | 
    
         
            +
            def parse_bpnet_output(
         
     | 
| 
      
 467 
     | 
    
         
            +
                file_paths: List[str], structure3d: Structure3D
         
     | 
| 
      
 468 
     | 
    
         
            +
            ) -> BaseInteractions:
         
     | 
| 
       455 
469 
     | 
    
         
             
                """
         
     | 
| 
       456 
470 
     | 
    
         
             
                Parse BPNet output files and convert to BaseInteractions.
         
     | 
| 
       457 
471 
     | 
    
         | 
| 
         @@ -649,7 +663,8 @@ def parse_bpnet_output(file_paths: List[str]) -> BaseInteractions: 
     | 
|
| 
       649 
663 
     | 
    
         
             
                            f"Error processing BPNet rob file {rob_file}: {e}", exc_info=True
         
     | 
| 
       650 
664 
     | 
    
         
             
                        )
         
     | 
| 
       651 
665 
     | 
    
         | 
| 
       652 
     | 
    
         
            -
                return BaseInteractions(
         
     | 
| 
      
 666 
     | 
    
         
            +
                return BaseInteractions.from_structure3d(
         
     | 
| 
      
 667 
     | 
    
         
            +
                    structure3d,
         
     | 
| 
       653 
668 
     | 
    
         
             
                    base_pairs,
         
     | 
| 
       654 
669 
     | 
    
         
             
                    stackings,
         
     | 
| 
       655 
670 
     | 
    
         
             
                    base_ribose_interactions,
         
     | 
| 
         @@ -986,7 +1001,8 @@ def parse_rnaview_output( 
     | 
|
| 
       986 
1001 
     | 
    
         
             
                except Exception as e:
         
     | 
| 
       987 
1002 
     | 
    
         
             
                    logging.warning(f"Error processing RNAView file {out_file}: {e}", exc_info=True)
         
     | 
| 
       988 
1003 
     | 
    
         | 
| 
       989 
     | 
    
         
            -
                return BaseInteractions(
         
     | 
| 
      
 1004 
     | 
    
         
            +
                return BaseInteractions.from_structure3d(
         
     | 
| 
      
 1005 
     | 
    
         
            +
                    structure3d,
         
     | 
| 
       990 
1006 
     | 
    
         
             
                    base_pairs,
         
     | 
| 
       991 
1007 
     | 
    
         
             
                    stackings,
         
     | 
| 
       992 
1008 
     | 
    
         
             
                    base_ribose_interactions,
         
     | 
| 
         @@ -1142,7 +1158,349 @@ def parse_barnaba_output( 
     | 
|
| 
       1142 
1158 
     | 
    
         
             
                                    f"Unknown barnaba stacking topology: {interaction_str}"
         
     | 
| 
       1143 
1159 
     | 
    
         
             
                                )
         
     | 
| 
       1144 
1160 
     | 
    
         | 
| 
       1145 
     | 
    
         
            -
                return BaseInteractions( 
     | 
| 
      
 1161 
     | 
    
         
            +
                return BaseInteractions.from_structure3d(
         
     | 
| 
      
 1162 
     | 
    
         
            +
                    structure3d, base_pairs, stackings, [], [], other_interactions
         
     | 
| 
      
 1163 
     | 
    
         
            +
                )
         
     | 
| 
      
 1164 
     | 
    
         
            +
             
     | 
| 
      
 1165 
     | 
    
         
            +
             
     | 
| 
      
 1166 
     | 
    
         
            +
            class MCAnnotateAdapter:
         
     | 
| 
      
 1167 
     | 
    
         
            +
                # Represents state of parsing MC-Annotate result
         
     | 
| 
      
 1168 
     | 
    
         
            +
                # Luckily every important part of file
         
     | 
| 
      
 1169 
     | 
    
         
            +
                # begins with a unique sentence
         
     | 
| 
      
 1170 
     | 
    
         
            +
                class ParseState(str, Enum):
         
     | 
| 
      
 1171 
     | 
    
         
            +
                    RESIDUES_INFORMATION = "Residue conformations"
         
     | 
| 
      
 1172 
     | 
    
         
            +
                    ADJACENT_STACKINGS = "Adjacent stackings"
         
     | 
| 
      
 1173 
     | 
    
         
            +
                    NON_ADJACENT_STACKINGS = "Non-Adjacent stackings"
         
     | 
| 
      
 1174 
     | 
    
         
            +
                    BASE_PAIRS_SECTION = "Base-pairs"
         
     | 
| 
      
 1175 
     | 
    
         
            +
                    SUMMARY_SECTION = "Number of"
         
     | 
| 
      
 1176 
     | 
    
         
            +
             
     | 
| 
      
 1177 
     | 
    
         
            +
                # This dictionary maps our model edges
         
     | 
| 
      
 1178 
     | 
    
         
            +
                # to edge representation used by MC-Annotate
         
     | 
| 
      
 1179 
     | 
    
         
            +
                EDGES: Dict[str, Tuple[str, ...]] = {
         
     | 
| 
      
 1180 
     | 
    
         
            +
                    "H": ("Hh", "Hw", "Bh", "C8"),
         
     | 
| 
      
 1181 
     | 
    
         
            +
                    "W": ("Wh", "Ww", "Ws"),
         
     | 
| 
      
 1182 
     | 
    
         
            +
                    "S": ("Ss", "Sw", "Bs"),
         
     | 
| 
      
 1183 
     | 
    
         
            +
                }
         
     | 
| 
      
 1184 
     | 
    
         
            +
             
     | 
| 
      
 1185 
     | 
    
         
            +
                # Contains flatten EDGES values (in one touple)
         
     | 
| 
      
 1186 
     | 
    
         
            +
                ALL_EDGES = sum(EDGES.values(), ())
         
     | 
| 
      
 1187 
     | 
    
         
            +
             
     | 
| 
      
 1188 
     | 
    
         
            +
                # Based on these tokens
         
     | 
| 
      
 1189 
     | 
    
         
            +
                # BaseRiboseInteractions and BasePhosphateInteractions are created
         
     | 
| 
      
 1190 
     | 
    
         
            +
                RIBOSE_ATOM = "O2'"
         
     | 
| 
      
 1191 
     | 
    
         
            +
                PHOSPHATE_ATOM = "O2P"
         
     | 
| 
      
 1192 
     | 
    
         
            +
             
     | 
| 
      
 1193 
     | 
    
         
            +
                # Single hydrogen bond - for us it's OtherInteraction
         
     | 
| 
      
 1194 
     | 
    
         
            +
                ONE_HBOND = "one_hbond"
         
     | 
| 
      
 1195 
     | 
    
         
            +
             
     | 
| 
      
 1196 
     | 
    
         
            +
                # Cis/trans tokens used by MC-Annotate
         
     | 
| 
      
 1197 
     | 
    
         
            +
                CIS = "cis"
         
     | 
| 
      
 1198 
     | 
    
         
            +
                TRANS = "trans"
         
     | 
| 
      
 1199 
     | 
    
         
            +
             
     | 
| 
      
 1200 
     | 
    
         
            +
                # Tokens used in PDB files
         
     | 
| 
      
 1201 
     | 
    
         
            +
                ATOM = "ATOM"
         
     | 
| 
      
 1202 
     | 
    
         
            +
                HETATM = "HETATM"
         
     | 
| 
      
 1203 
     | 
    
         
            +
             
     | 
| 
      
 1204 
     | 
    
         
            +
                # This regex is used to capture 6 groups of residues information:
         
     | 
| 
      
 1205 
     | 
    
         
            +
                # (1) (2) (3) (4) (5) (6)
         
     | 
| 
      
 1206 
     | 
    
         
            +
                # 1, 4 - chain IDs
         
     | 
| 
      
 1207 
     | 
    
         
            +
                # 2, 5 - numbers
         
     | 
| 
      
 1208 
     | 
    
         
            +
                # 3, 6 - icodes (or empty string if no icode)
         
     | 
| 
      
 1209 
     | 
    
         
            +
                # Example - match and groups:
         
     | 
| 
      
 1210 
     | 
    
         
            +
                # A-100.X-B200
         
     | 
| 
      
 1211 
     | 
    
         
            +
                # ('A'), ('-100'), ('X'), ('B'), ('200'), ('')
         
     | 
| 
      
 1212 
     | 
    
         
            +
                RESIDUE_REGEX = re.compile(
         
     | 
| 
      
 1213 
     | 
    
         
            +
                    r"'?(.)'?(-?[0-9]+)\.?([a-zA-Z]?)-'?(.)'?(-?[0-9]+)\.?([a-zA-Z]?)"
         
     | 
| 
      
 1214 
     | 
    
         
            +
                )
         
     | 
| 
      
 1215 
     | 
    
         
            +
             
     | 
| 
      
 1216 
     | 
    
         
            +
                # Roman numerals used by Saenger
         
     | 
| 
      
 1217 
     | 
    
         
            +
                # both in our model and MC-Annotate
         
     | 
| 
      
 1218 
     | 
    
         
            +
                ROMAN_NUMERALS = ("I", "V", "X")
         
     | 
| 
      
 1219 
     | 
    
         
            +
             
     | 
| 
      
 1220 
     | 
    
         
            +
                # Positions of residues info in PDB files
         
     | 
| 
      
 1221 
     | 
    
         
            +
                CHAIN_INDEX = 21
         
     | 
| 
      
 1222 
     | 
    
         
            +
                NUMBER_INDEX = slice(22, 26)
         
     | 
| 
      
 1223 
     | 
    
         
            +
                ICODE_INDEX = 26
         
     | 
| 
      
 1224 
     | 
    
         
            +
                NAME_INDEX = slice(17, 20)
         
     | 
| 
      
 1225 
     | 
    
         
            +
             
     | 
| 
      
 1226 
     | 
    
         
            +
                def __init__(self) -> None:
         
     | 
| 
      
 1227 
     | 
    
         
            +
                    # Since names are not present in adjacent and non-adjacent stackings
         
     | 
| 
      
 1228 
     | 
    
         
            +
                    # we need save these values eariler
         
     | 
| 
      
 1229 
     | 
    
         
            +
                    self.names: Dict[str, str] = {}
         
     | 
| 
      
 1230 
     | 
    
         
            +
                    self.base_pairs: List[BasePair] = []
         
     | 
| 
      
 1231 
     | 
    
         
            +
                    self.stackings: List[Stacking] = []
         
     | 
| 
      
 1232 
     | 
    
         
            +
                    self.base_ribose_interactions: List[BaseRibose] = []
         
     | 
| 
      
 1233 
     | 
    
         
            +
                    self.base_phosphate_interactions: List[BasePhosphate] = []
         
     | 
| 
      
 1234 
     | 
    
         
            +
                    self.other_interactions: List[OtherInteraction] = []
         
     | 
| 
      
 1235 
     | 
    
         
            +
             
     | 
| 
      
 1236 
     | 
    
         
            +
                def classify_edge(self, edge_type: str) -> Optional[str]:
         
     | 
| 
      
 1237 
     | 
    
         
            +
                    for edge, edges in self.EDGES.items():
         
     | 
| 
      
 1238 
     | 
    
         
            +
                        if edge_type in edges:
         
     | 
| 
      
 1239 
     | 
    
         
            +
                            return edge
         
     | 
| 
      
 1240 
     | 
    
         
            +
                    logging.warning('Edge type "{type}" unknown')
         
     | 
| 
      
 1241 
     | 
    
         
            +
                    return None
         
     | 
| 
      
 1242 
     | 
    
         
            +
             
     | 
| 
      
 1243 
     | 
    
         
            +
                def get_residue(self, residue_info_list: Tuple[Union[str, Any], ...]) -> Residue:
         
     | 
| 
      
 1244 
     | 
    
         
            +
                    chain = residue_info_list[0]
         
     | 
| 
      
 1245 
     | 
    
         
            +
                    number = int(residue_info_list[1])
         
     | 
| 
      
 1246 
     | 
    
         
            +
             
     | 
| 
      
 1247 
     | 
    
         
            +
                    if residue_info_list[2] == "":
         
     | 
| 
      
 1248 
     | 
    
         
            +
                        icode = None
         
     | 
| 
      
 1249 
     | 
    
         
            +
                        residue_info = f"{chain}{number}"
         
     | 
| 
      
 1250 
     | 
    
         
            +
                    else:
         
     | 
| 
      
 1251 
     | 
    
         
            +
                        icode = residue_info_list[2]
         
     | 
| 
      
 1252 
     | 
    
         
            +
                        residue_info = f"{chain}{number}.{icode}"
         
     | 
| 
      
 1253 
     | 
    
         
            +
             
     | 
| 
      
 1254 
     | 
    
         
            +
                    return Residue(
         
     | 
| 
      
 1255 
     | 
    
         
            +
                        None, ResidueAuth(chain, number, icode, self.names[residue_info])
         
     | 
| 
      
 1256 
     | 
    
         
            +
                    )
         
     | 
| 
      
 1257 
     | 
    
         
            +
             
     | 
| 
      
 1258 
     | 
    
         
            +
                def get_residues(
         
     | 
| 
      
 1259 
     | 
    
         
            +
                    self, residues_info: str
         
     | 
| 
      
 1260 
     | 
    
         
            +
                ) -> Tuple[Optional[Residue], Optional[Residue]]:
         
     | 
| 
      
 1261 
     | 
    
         
            +
                    regex_result = re.search(self.RESIDUE_REGEX, residues_info)
         
     | 
| 
      
 1262 
     | 
    
         
            +
                    if regex_result is None:
         
     | 
| 
      
 1263 
     | 
    
         
            +
                        logging.error("MC-Annotate regex failed: {residues_info}")
         
     | 
| 
      
 1264 
     | 
    
         
            +
                        return None, None
         
     | 
| 
      
 1265 
     | 
    
         
            +
                    residues_info_list = regex_result.groups()
         
     | 
| 
      
 1266 
     | 
    
         
            +
                    # Expects (chain1, number1, icode1, chain2, number2, icode2)
         
     | 
| 
      
 1267 
     | 
    
         
            +
                    if len(residues_info_list) != 6:
         
     | 
| 
      
 1268 
     | 
    
         
            +
                        logging.error(f"MC-Annotate regex failed for {residues_info}")
         
     | 
| 
      
 1269 
     | 
    
         
            +
                        return None, None
         
     | 
| 
      
 1270 
     | 
    
         
            +
                    residue_left = self.get_residue(residues_info_list[:3])
         
     | 
| 
      
 1271 
     | 
    
         
            +
                    residue_right = self.get_residue(residues_info_list[3:])
         
     | 
| 
      
 1272 
     | 
    
         
            +
                    return residue_left, residue_right
         
     | 
| 
      
 1273 
     | 
    
         
            +
             
     | 
| 
      
 1274 
     | 
    
         
            +
                def append_stacking(self, line: str, topology_position: int) -> None:
         
     | 
| 
      
 1275 
     | 
    
         
            +
                    splitted_line = line.split()
         
     | 
| 
      
 1276 
     | 
    
         
            +
                    topology_info = splitted_line[topology_position]
         
     | 
| 
      
 1277 
     | 
    
         
            +
                    residue_left, residue_right = self.get_residues(splitted_line[0])
         
     | 
| 
      
 1278 
     | 
    
         
            +
                    if residue_left is None or residue_right is None:
         
     | 
| 
      
 1279 
     | 
    
         
            +
                        logging.warning(f"Could not parse residues in line: {line}")
         
     | 
| 
      
 1280 
     | 
    
         
            +
                        return
         
     | 
| 
      
 1281 
     | 
    
         
            +
                    stacking = Stacking(
         
     | 
| 
      
 1282 
     | 
    
         
            +
                        residue_left, residue_right, StackingTopology[topology_info]
         
     | 
| 
      
 1283 
     | 
    
         
            +
                    )
         
     | 
| 
      
 1284 
     | 
    
         
            +
                    self.stackings.append(stacking)
         
     | 
| 
      
 1285 
     | 
    
         
            +
             
     | 
| 
      
 1286 
     | 
    
         
            +
                def get_ribose_interaction(
         
     | 
| 
      
 1287 
     | 
    
         
            +
                    self, residues: Tuple[Residue, Residue], token: str
         
     | 
| 
      
 1288 
     | 
    
         
            +
                ) -> BaseRibose:
         
     | 
| 
      
 1289 
     | 
    
         
            +
                    # BasePair is preffered first so swap if necessary
         
     | 
| 
      
 1290 
     | 
    
         
            +
                    if token.split("/", 1)[0] == self.RIBOSE_ATOM:
         
     | 
| 
      
 1291 
     | 
    
         
            +
                        residue_left, residue_right = residues[1], residues[0]
         
     | 
| 
      
 1292 
     | 
    
         
            +
                    else:
         
     | 
| 
      
 1293 
     | 
    
         
            +
                        residue_left, residue_right = residues[0], residues[1]
         
     | 
| 
      
 1294 
     | 
    
         
            +
                    return BaseRibose(residue_left, residue_right, None)
         
     | 
| 
      
 1295 
     | 
    
         
            +
             
     | 
| 
      
 1296 
     | 
    
         
            +
                def get_phosphate_interaction(
         
     | 
| 
      
 1297 
     | 
    
         
            +
                    self, residues: Tuple[Residue, Residue], token: str
         
     | 
| 
      
 1298 
     | 
    
         
            +
                ) -> BasePhosphate:
         
     | 
| 
      
 1299 
     | 
    
         
            +
                    # BasePair is preffered first so swap if necessary
         
     | 
| 
      
 1300 
     | 
    
         
            +
                    if token.split("/", 1)[0] == self.PHOSPHATE_ATOM:
         
     | 
| 
      
 1301 
     | 
    
         
            +
                        residue_left, residue_right = residues[1], residues[0]
         
     | 
| 
      
 1302 
     | 
    
         
            +
                    else:
         
     | 
| 
      
 1303 
     | 
    
         
            +
                        residue_left, residue_right = residues[0], residues[1]
         
     | 
| 
      
 1304 
     | 
    
         
            +
                    return BasePhosphate(residue_left, residue_right, None)
         
     | 
| 
      
 1305 
     | 
    
         
            +
             
     | 
| 
      
 1306 
     | 
    
         
            +
                def get_base_interaction(
         
     | 
| 
      
 1307 
     | 
    
         
            +
                    self,
         
     | 
| 
      
 1308 
     | 
    
         
            +
                    residues: Tuple[Residue, Residue],
         
     | 
| 
      
 1309 
     | 
    
         
            +
                    token: str,
         
     | 
| 
      
 1310 
     | 
    
         
            +
                    tokens: List[str],
         
     | 
| 
      
 1311 
     | 
    
         
            +
                ) -> Optional[BasePair]:
         
     | 
| 
      
 1312 
     | 
    
         
            +
                    if self.CIS in tokens:
         
     | 
| 
      
 1313 
     | 
    
         
            +
                        cis_trans = "c"
         
     | 
| 
      
 1314 
     | 
    
         
            +
                    elif self.TRANS in tokens:
         
     | 
| 
      
 1315 
     | 
    
         
            +
                        cis_trans = "t"
         
     | 
| 
      
 1316 
     | 
    
         
            +
                    else:
         
     | 
| 
      
 1317 
     | 
    
         
            +
                        logging.warning(f"Cis/trans expected, but not present in {tokens}")
         
     | 
| 
      
 1318 
     | 
    
         
            +
                        return None
         
     | 
| 
      
 1319 
     | 
    
         
            +
             
     | 
| 
      
 1320 
     | 
    
         
            +
                    # example saenger: XIX or XII,XIII (?)
         
     | 
| 
      
 1321 
     | 
    
         
            +
                    for potential_saenger_token in tokens:
         
     | 
| 
      
 1322 
     | 
    
         
            +
                        potential_saenger_without_comma = potential_saenger_token.split(",")[0]
         
     | 
| 
      
 1323 
     | 
    
         
            +
                        if all(
         
     | 
| 
      
 1324 
     | 
    
         
            +
                            char in self.ROMAN_NUMERALS for char in potential_saenger_without_comma
         
     | 
| 
      
 1325 
     | 
    
         
            +
                        ):
         
     | 
| 
      
 1326 
     | 
    
         
            +
                            saenger = Saenger[potential_saenger_without_comma]
         
     | 
| 
      
 1327 
     | 
    
         
            +
                            break
         
     | 
| 
      
 1328 
     | 
    
         
            +
                    else:
         
     | 
| 
      
 1329 
     | 
    
         
            +
                        saenger = None
         
     | 
| 
      
 1330 
     | 
    
         
            +
             
     | 
| 
      
 1331 
     | 
    
         
            +
                    left_edge, right_edge = token.split("/", 1)
         
     | 
| 
      
 1332 
     | 
    
         
            +
                    leontis_westhof_left = self.classify_edge(left_edge)
         
     | 
| 
      
 1333 
     | 
    
         
            +
                    leontis_westohf_right = self.classify_edge(right_edge)
         
     | 
| 
      
 1334 
     | 
    
         
            +
             
     | 
| 
      
 1335 
     | 
    
         
            +
                    if leontis_westhof_left is None or leontis_westohf_right is None:
         
     | 
| 
      
 1336 
     | 
    
         
            +
                        return None
         
     | 
| 
      
 1337 
     | 
    
         
            +
             
     | 
| 
      
 1338 
     | 
    
         
            +
                    leontis_westhof = LeontisWesthof[
         
     | 
| 
      
 1339 
     | 
    
         
            +
                        f"{cis_trans}{leontis_westhof_left}{leontis_westohf_right}"
         
     | 
| 
      
 1340 
     | 
    
         
            +
                    ]
         
     | 
| 
      
 1341 
     | 
    
         
            +
                    residue_left, residue_right = residues
         
     | 
| 
      
 1342 
     | 
    
         
            +
                    return BasePair(residue_left, residue_right, leontis_westhof, saenger)
         
     | 
| 
      
 1343 
     | 
    
         
            +
             
     | 
| 
      
 1344 
     | 
    
         
            +
                def get_other_interaction(
         
     | 
| 
      
 1345 
     | 
    
         
            +
                    self, residues: Tuple[Residue, Residue]
         
     | 
| 
      
 1346 
     | 
    
         
            +
                ) -> OtherInteraction:
         
     | 
| 
      
 1347 
     | 
    
         
            +
                    return OtherInteraction(residues[0], residues[1])
         
     | 
| 
      
 1348 
     | 
    
         
            +
             
     | 
| 
      
 1349 
     | 
    
         
            +
                def append_interactions(self, line: str) -> None:
         
     | 
| 
      
 1350 
     | 
    
         
            +
                    splitted_line = line.split()
         
     | 
| 
      
 1351 
     | 
    
         
            +
                    residues = self.get_residues(splitted_line[0])
         
     | 
| 
      
 1352 
     | 
    
         
            +
                    if residues[0] is None or residues[1] is None:
         
     | 
| 
      
 1353 
     | 
    
         
            +
                        logging.warning(f"Could not parse residues in line: {line}")
         
     | 
| 
      
 1354 
     | 
    
         
            +
                        return
         
     | 
| 
      
 1355 
     | 
    
         
            +
                    # Assumes that one pair can belong to every interaction type
         
     | 
| 
      
 1356 
     | 
    
         
            +
                    # no more than once!
         
     | 
| 
      
 1357 
     | 
    
         
            +
                    base_added, ribose_added, phosphate_added = False, False, False
         
     | 
| 
      
 1358 
     | 
    
         
            +
                    # example tokens: Ww/Ww pairing antiparallel cis XX
         
     | 
| 
      
 1359 
     | 
    
         
            +
                    tokens: List[str] = splitted_line[3:]
         
     | 
| 
      
 1360 
     | 
    
         
            +
             
     | 
| 
      
 1361 
     | 
    
         
            +
                    # Special case
         
     | 
| 
      
 1362 
     | 
    
         
            +
                    # IF single hydrogen bond and base pairs only THEN
         
     | 
| 
      
 1363 
     | 
    
         
            +
                    # append to OtherIneraction list
         
     | 
| 
      
 1364 
     | 
    
         
            +
                    if self.ONE_HBOND in tokens:
         
     | 
| 
      
 1365 
     | 
    
         
            +
                        for token in tokens:
         
     | 
| 
      
 1366 
     | 
    
         
            +
                            if self.RIBOSE_ATOM in token or self.PHOSPHATE_ATOM in token:
         
     | 
| 
      
 1367 
     | 
    
         
            +
                                break
         
     | 
| 
      
 1368 
     | 
    
         
            +
                        else:
         
     | 
| 
      
 1369 
     | 
    
         
            +
                            other_interaction = self.get_other_interaction(residues)
         
     | 
| 
      
 1370 
     | 
    
         
            +
                            self.other_interactions.append(other_interaction)
         
     | 
| 
      
 1371 
     | 
    
         
            +
                            return
         
     | 
| 
      
 1372 
     | 
    
         
            +
             
     | 
| 
      
 1373 
     | 
    
         
            +
                    for token in tokens:
         
     | 
| 
      
 1374 
     | 
    
         
            +
                        if self.RIBOSE_ATOM in token and not ribose_added:
         
     | 
| 
      
 1375 
     | 
    
         
            +
                            # example token: Ss/O2'
         
     | 
| 
      
 1376 
     | 
    
         
            +
                            ribose_interaction = self.get_ribose_interaction(residues, token)
         
     | 
| 
      
 1377 
     | 
    
         
            +
                            self.base_ribose_interactions.append(ribose_interaction)
         
     | 
| 
      
 1378 
     | 
    
         
            +
                            ribose_added = True
         
     | 
| 
      
 1379 
     | 
    
         
            +
             
     | 
| 
      
 1380 
     | 
    
         
            +
                        elif self.PHOSPHATE_ATOM in token and not phosphate_added:
         
     | 
| 
      
 1381 
     | 
    
         
            +
                            # example token: O2P/Bh
         
     | 
| 
      
 1382 
     | 
    
         
            +
                            phosphate_interaction = self.get_phosphate_interaction(residues, token)
         
     | 
| 
      
 1383 
     | 
    
         
            +
                            self.base_phosphate_interactions.append(phosphate_interaction)
         
     | 
| 
      
 1384 
     | 
    
         
            +
                            phosphate_added = True
         
     | 
| 
      
 1385 
     | 
    
         
            +
             
     | 
| 
      
 1386 
     | 
    
         
            +
                        elif len(token.split("/", 1)) > 1:
         
     | 
| 
      
 1387 
     | 
    
         
            +
                            token_left, token_right = token.split("/", 1)
         
     | 
| 
      
 1388 
     | 
    
         
            +
                            tokens_in_edges = (
         
     | 
| 
      
 1389 
     | 
    
         
            +
                                token_left in self.ALL_EDGES and token_right in self.ALL_EDGES
         
     | 
| 
      
 1390 
     | 
    
         
            +
                            )
         
     | 
| 
      
 1391 
     | 
    
         
            +
                            if tokens_in_edges and not base_added:
         
     | 
| 
      
 1392 
     | 
    
         
            +
                                # example token_left: Ww | example token_right: Ws
         
     | 
| 
      
 1393 
     | 
    
         
            +
                                base_pair_interaction = self.get_base_interaction(
         
     | 
| 
      
 1394 
     | 
    
         
            +
                                    residues, token, tokens
         
     | 
| 
      
 1395 
     | 
    
         
            +
                                )
         
     | 
| 
      
 1396 
     | 
    
         
            +
                                if base_pair_interaction is not None:
         
     | 
| 
      
 1397 
     | 
    
         
            +
                                    self.base_pairs.append(base_pair_interaction)
         
     | 
| 
      
 1398 
     | 
    
         
            +
                                base_added = True
         
     | 
| 
      
 1399 
     | 
    
         
            +
             
     | 
| 
      
 1400 
     | 
    
         
            +
                def append_names(self, file_content: str) -> None:
         
     | 
| 
      
 1401 
     | 
    
         
            +
                    for line in file_content.splitlines():
         
     | 
| 
      
 1402 
     | 
    
         
            +
                        if line.startswith(self.ATOM) or line.startswith(self.HETATM):
         
     | 
| 
      
 1403 
     | 
    
         
            +
                            chain = line[self.CHAIN_INDEX].strip()
         
     | 
| 
      
 1404 
     | 
    
         
            +
                            number = line[self.NUMBER_INDEX].strip()
         
     | 
| 
      
 1405 
     | 
    
         
            +
                            icode = line[self.ICODE_INDEX].strip()
         
     | 
| 
      
 1406 
     | 
    
         
            +
                            name = line[self.NAME_INDEX].strip()
         
     | 
| 
      
 1407 
     | 
    
         
            +
                            residue_info = (
         
     | 
| 
      
 1408 
     | 
    
         
            +
                                f"{chain}{number}" if icode == "" else f"{chain}{number}.{icode}"
         
     | 
| 
      
 1409 
     | 
    
         
            +
                            )
         
     | 
| 
      
 1410 
     | 
    
         
            +
                            self.names[residue_info] = name
         
     | 
| 
      
 1411 
     | 
    
         
            +
             
     | 
| 
      
 1412 
     | 
    
         
            +
                def analyze_by_mc_annotate(
         
     | 
| 
      
 1413 
     | 
    
         
            +
                    self, pdb_content: str, mc_result: str, **_: Dict[str, Any]
         
     | 
| 
      
 1414 
     | 
    
         
            +
                ) -> BaseInteractions:
         
     | 
| 
      
 1415 
     | 
    
         
            +
                    self.append_names(pdb_content)
         
     | 
| 
      
 1416 
     | 
    
         
            +
                    current_state = None
         
     | 
| 
      
 1417 
     | 
    
         
            +
             
     | 
| 
      
 1418 
     | 
    
         
            +
                    for line in mc_result.splitlines():
         
     | 
| 
      
 1419 
     | 
    
         
            +
                        for state in self.ParseState:
         
     | 
| 
      
 1420 
     | 
    
         
            +
                            if line.startswith(state.value):
         
     | 
| 
      
 1421 
     | 
    
         
            +
                                current_state = state
         
     | 
| 
      
 1422 
     | 
    
         
            +
                                break
         
     | 
| 
      
 1423 
     | 
    
         
            +
                        # Loop ended without break - parse file
         
     | 
| 
      
 1424 
     | 
    
         
            +
                        else:
         
     | 
| 
      
 1425 
     | 
    
         
            +
                            if current_state == self.ParseState.RESIDUES_INFORMATION:
         
     | 
| 
      
 1426 
     | 
    
         
            +
                                # example line: X7.H : G C3p_endo anti
         
     | 
| 
      
 1427 
     | 
    
         
            +
                                # Skip residues information - meaningless information
         
     | 
| 
      
 1428 
     | 
    
         
            +
                                pass
         
     | 
| 
      
 1429 
     | 
    
         
            +
                            elif current_state == self.ParseState.ADJACENT_STACKINGS:
         
     | 
| 
      
 1430 
     | 
    
         
            +
                                # example line: X4.E-X5.F : adjacent_5p upward
         
     | 
| 
      
 1431 
     | 
    
         
            +
                                self.append_stacking(line, 3)
         
     | 
| 
      
 1432 
     | 
    
         
            +
                            elif current_state == self.ParseState.NON_ADJACENT_STACKINGS:
         
     | 
| 
      
 1433 
     | 
    
         
            +
                                # example line: Y40.M-Y67.N : inward pairing
         
     | 
| 
      
 1434 
     | 
    
         
            +
                                self.append_stacking(line, 2)
         
     | 
| 
      
 1435 
     | 
    
         
            +
                            elif current_state == self.ParseState.BASE_PAIRS_SECTION:
         
     | 
| 
      
 1436 
     | 
    
         
            +
                                # example line: Y38.K-Y51.X : A-U Ww/Ww pairing antiparallel cis XX
         
     | 
| 
      
 1437 
     | 
    
         
            +
                                self.append_interactions(line)
         
     | 
| 
      
 1438 
     | 
    
         
            +
                            elif current_state == self.ParseState.SUMMARY_SECTION:
         
     | 
| 
      
 1439 
     | 
    
         
            +
                                # example line: Number of non adjacent stackings = 26
         
     | 
| 
      
 1440 
     | 
    
         
            +
                                # Skip summary section - meaningless information
         
     | 
| 
      
 1441 
     | 
    
         
            +
                                pass
         
     | 
| 
      
 1442 
     | 
    
         
            +
             
     | 
| 
      
 1443 
     | 
    
         
            +
                    return (
         
     | 
| 
      
 1444 
     | 
    
         
            +
                        self.base_pairs,
         
     | 
| 
      
 1445 
     | 
    
         
            +
                        self.stackings,
         
     | 
| 
      
 1446 
     | 
    
         
            +
                        self.base_ribose_interactions,
         
     | 
| 
      
 1447 
     | 
    
         
            +
                        self.base_phosphate_interactions,
         
     | 
| 
      
 1448 
     | 
    
         
            +
                        self.other_interactions,
         
     | 
| 
      
 1449 
     | 
    
         
            +
                    )
         
     | 
| 
      
 1450 
     | 
    
         
            +
             
     | 
| 
      
 1451 
     | 
    
         
            +
             
     | 
| 
      
 1452 
     | 
    
         
            +
            def parse_mcannotate_output(
         
     | 
| 
      
 1453 
     | 
    
         
            +
                file_paths: List[str], structure3d: Structure3D
         
     | 
| 
      
 1454 
     | 
    
         
            +
            ) -> BaseInteractions:
         
     | 
| 
      
 1455 
     | 
    
         
            +
                """
         
     | 
| 
      
 1456 
     | 
    
         
            +
                Parse mc-annotate output and convert to BaseInteractions.
         
     | 
| 
      
 1457 
     | 
    
         
            +
                This function expects a file with mc-annotate stdout and a PDB file.
         
     | 
| 
      
 1458 
     | 
    
         
            +
                """
         
     | 
| 
      
 1459 
     | 
    
         
            +
                stdout_file = None
         
     | 
| 
      
 1460 
     | 
    
         
            +
                structure_file = None
         
     | 
| 
      
 1461 
     | 
    
         
            +
                for file_path in file_paths:
         
     | 
| 
      
 1462 
     | 
    
         
            +
                    if os.path.basename(file_path).endswith("stdout.txt"):
         
     | 
| 
      
 1463 
     | 
    
         
            +
                        stdout_file = file_path
         
     | 
| 
      
 1464 
     | 
    
         
            +
                    elif file_path.endswith(".pdb"):
         
     | 
| 
      
 1465 
     | 
    
         
            +
                        structure_file = file_path
         
     | 
| 
      
 1466 
     | 
    
         
            +
             
     | 
| 
      
 1467 
     | 
    
         
            +
                if not stdout_file:
         
     | 
| 
      
 1468 
     | 
    
         
            +
                    logging.warning("No stdout.txt file found for mc-annotate.")
         
     | 
| 
      
 1469 
     | 
    
         
            +
                    return BaseInteractions([], [], [], [], [])
         
     | 
| 
      
 1470 
     | 
    
         
            +
             
     | 
| 
      
 1471 
     | 
    
         
            +
                if not structure_file:
         
     | 
| 
      
 1472 
     | 
    
         
            +
                    logging.warning("No PDB file found for mc-annotate.")
         
     | 
| 
      
 1473 
     | 
    
         
            +
                    return BaseInteractions([], [], [], [], [])
         
     | 
| 
      
 1474 
     | 
    
         
            +
             
     | 
| 
      
 1475 
     | 
    
         
            +
                logging.info(f"Processing mc-annotate stdout file: {stdout_file}")
         
     | 
| 
      
 1476 
     | 
    
         
            +
                logging.info(f"Using structure file for residue names: {structure_file}")
         
     | 
| 
      
 1477 
     | 
    
         
            +
             
     | 
| 
      
 1478 
     | 
    
         
            +
                try:
         
     | 
| 
      
 1479 
     | 
    
         
            +
                    with open(stdout_file, "r") as f:
         
     | 
| 
      
 1480 
     | 
    
         
            +
                        mc_result = f.read()
         
     | 
| 
      
 1481 
     | 
    
         
            +
                    with open(structure_file, "r") as f:
         
     | 
| 
      
 1482 
     | 
    
         
            +
                        pdb_content = f.read()
         
     | 
| 
      
 1483 
     | 
    
         
            +
                except Exception as e:
         
     | 
| 
      
 1484 
     | 
    
         
            +
                    logging.warning(f"Could not read input files for mc-annotate: {e}")
         
     | 
| 
      
 1485 
     | 
    
         
            +
                    return BaseInteractions([], [], [], [], [])
         
     | 
| 
      
 1486 
     | 
    
         
            +
             
     | 
| 
      
 1487 
     | 
    
         
            +
                adapter = MCAnnotateAdapter()
         
     | 
| 
      
 1488 
     | 
    
         
            +
                (
         
     | 
| 
      
 1489 
     | 
    
         
            +
                    base_pairs,
         
     | 
| 
      
 1490 
     | 
    
         
            +
                    stackings,
         
     | 
| 
      
 1491 
     | 
    
         
            +
                    base_ribose_interactions,
         
     | 
| 
      
 1492 
     | 
    
         
            +
                    base_phosphate_interactions,
         
     | 
| 
      
 1493 
     | 
    
         
            +
                    other_interactions,
         
     | 
| 
      
 1494 
     | 
    
         
            +
                ) = adapter.analyze_by_mc_annotate(pdb_content, mc_result)
         
     | 
| 
      
 1495 
     | 
    
         
            +
             
     | 
| 
      
 1496 
     | 
    
         
            +
                return BaseInteractions.from_structure3d(
         
     | 
| 
      
 1497 
     | 
    
         
            +
                    structure3d,
         
     | 
| 
      
 1498 
     | 
    
         
            +
                    base_pairs,
         
     | 
| 
      
 1499 
     | 
    
         
            +
                    stackings,
         
     | 
| 
      
 1500 
     | 
    
         
            +
                    base_ribose_interactions,
         
     | 
| 
      
 1501 
     | 
    
         
            +
                    base_phosphate_interactions,
         
     | 
| 
      
 1502 
     | 
    
         
            +
                    other_interactions,
         
     | 
| 
      
 1503 
     | 
    
         
            +
                )
         
     | 
| 
       1146 
1504 
     | 
    
         | 
| 
       1147 
1505 
     | 
    
         | 
| 
       1148 
1506 
     | 
    
         
             
            def parse_external_output(
         
     | 
| 
         @@ -1160,22 +1518,26 @@ def parse_external_output( 
     | 
|
| 
       1160 
1518 
     | 
    
         
             
                    BaseInteractions object containing the interactions found by the external tool
         
     | 
| 
       1161 
1519 
     | 
    
         
             
                """
         
     | 
| 
       1162 
1520 
     | 
    
         
             
                if tool == ExternalTool.FR3D:
         
     | 
| 
       1163 
     | 
    
         
            -
                    return parse_fr3d_output(file_paths)
         
     | 
| 
      
 1521 
     | 
    
         
            +
                    return parse_fr3d_output(file_paths, structure3d)
         
     | 
| 
       1164 
1522 
     | 
    
         
             
                elif tool == ExternalTool.DSSR:
         
     | 
| 
       1165 
1523 
     | 
    
         
             
                    return parse_dssr_output(file_paths, structure3d)
         
     | 
| 
       1166 
1524 
     | 
    
         
             
                elif tool == ExternalTool.MAXIT:
         
     | 
| 
       1167 
     | 
    
         
            -
                    return parse_maxit_output(file_paths)
         
     | 
| 
      
 1525 
     | 
    
         
            +
                    return parse_maxit_output(file_paths, structure3d)
         
     | 
| 
       1168 
1526 
     | 
    
         
             
                elif tool == ExternalTool.BPNET:
         
     | 
| 
       1169 
     | 
    
         
            -
                    return parse_bpnet_output(file_paths)
         
     | 
| 
      
 1527 
     | 
    
         
            +
                    return parse_bpnet_output(file_paths, structure3d)
         
     | 
| 
       1170 
1528 
     | 
    
         
             
                elif tool == ExternalTool.RNAVIEW:
         
     | 
| 
       1171 
1529 
     | 
    
         
             
                    return parse_rnaview_output(file_paths, structure3d)
         
     | 
| 
       1172 
1530 
     | 
    
         
             
                elif tool == ExternalTool.BARNABA:
         
     | 
| 
       1173 
1531 
     | 
    
         
             
                    return parse_barnaba_output(file_paths, structure3d)
         
     | 
| 
      
 1532 
     | 
    
         
            +
                elif tool == ExternalTool.MCANNOTATE:
         
     | 
| 
      
 1533 
     | 
    
         
            +
                    return parse_mcannotate_output(file_paths, structure3d)
         
     | 
| 
       1174 
1534 
     | 
    
         
             
                else:
         
     | 
| 
       1175 
1535 
     | 
    
         
             
                    raise ValueError(f"Unsupported external tool: {tool}")
         
     | 
| 
       1176 
1536 
     | 
    
         | 
| 
       1177 
1537 
     | 
    
         | 
| 
       1178 
     | 
    
         
            -
            def parse_fr3d_output( 
     | 
| 
      
 1538 
     | 
    
         
            +
            def parse_fr3d_output(
         
     | 
| 
      
 1539 
     | 
    
         
            +
                file_paths: List[str], structure3d: Structure3D
         
     | 
| 
      
 1540 
     | 
    
         
            +
            ) -> BaseInteractions:
         
     | 
| 
       1179 
1541 
     | 
    
         
             
                """
         
     | 
| 
       1180 
1542 
     | 
    
         
             
                Parse FR3D output files and convert to BaseInteractions.
         
     | 
| 
       1181 
1543 
     | 
    
         | 
| 
         @@ -1208,7 +1570,8 @@ def parse_fr3d_output(file_paths: List[str]) -> BaseInteractions: 
     | 
|
| 
       1208 
1570 
     | 
    
         
             
                            _process_interaction_line(line, interactions_data)
         
     | 
| 
       1209 
1571 
     | 
    
         | 
| 
       1210 
1572 
     | 
    
         
             
                # Return a BaseInteractions object with all the processed interactions
         
     | 
| 
       1211 
     | 
    
         
            -
                return BaseInteractions(
         
     | 
| 
      
 1573 
     | 
    
         
            +
                return BaseInteractions.from_structure3d(
         
     | 
| 
      
 1574 
     | 
    
         
            +
                    structure3d,
         
     | 
| 
       1212 
1575 
     | 
    
         
             
                    interactions_data["base_pairs"],
         
     | 
| 
       1213 
1576 
     | 
    
         
             
                    interactions_data["stackings"],
         
     | 
| 
       1214 
1577 
     | 
    
         
             
                    interactions_data["base_ribose_interactions"],
         
     | 
| 
         @@ -1244,6 +1607,9 @@ def process_external_tool_output( 
     | 
|
| 
       1244 
1607 
     | 
    
         
             
                if not external_file_paths:
         
     | 
| 
       1245 
1608 
     | 
    
         
             
                    # For MAXIT or when no external files are provided, use the input file
         
     | 
| 
       1246 
1609 
     | 
    
         
             
                    file_paths_to_process = [input_file_path]
         
     | 
| 
      
 1610 
     | 
    
         
            +
                elif tool == ExternalTool.MCANNOTATE:
         
     | 
| 
      
 1611 
     | 
    
         
            +
                    # MC-Annotate requires both the stdout and the PDB file
         
     | 
| 
      
 1612 
     | 
    
         
            +
                    file_paths_to_process = external_file_paths + [input_file_path]
         
     | 
| 
       1247 
1613 
     | 
    
         
             
                else:
         
     | 
| 
       1248 
1614 
     | 
    
         
             
                    # Process all external files
         
     | 
| 
       1249 
1615 
     | 
    
         
             
                    file_paths_to_process = external_file_paths
         
     | 
    
        rnapolis/annotator.py
    CHANGED
    
    | 
         @@ -85,15 +85,6 @@ def detect_cis_trans(residue_i: Residue3D, residue_j: Residue3D) -> Optional[str 
     | 
|
| 
       85 
85 
     | 
    
         
             
                return "c" if -90.0 < torsion < 90.0 else "t"
         
     | 
| 
       86 
86 
     | 
    
         | 
| 
       87 
87 
     | 
    
         | 
| 
       88 
     | 
    
         
            -
            def detect_saenger(
         
     | 
| 
       89 
     | 
    
         
            -
                residue_i: Residue3D, residue_j: Residue3D, lw: LeontisWesthof
         
     | 
| 
       90 
     | 
    
         
            -
            ) -> Optional[Saenger]:
         
     | 
| 
       91 
     | 
    
         
            -
                key = (f"{residue_i.one_letter_name}{residue_j.one_letter_name}", lw.value)
         
     | 
| 
       92 
     | 
    
         
            -
                if key in Saenger.table():
         
     | 
| 
       93 
     | 
    
         
            -
                    return Saenger[Saenger.table()[key]]
         
     | 
| 
       94 
     | 
    
         
            -
                return None
         
     | 
| 
       95 
     | 
    
         
            -
             
     | 
| 
       96 
     | 
    
         
            -
             
     | 
| 
       97 
88 
     | 
    
         
             
            def detect_bph_br_classification(
         
     | 
| 
       98 
89 
     | 
    
         
             
                donor_residue: Residue3D, donor: Atom, acceptor: Atom
         
     | 
| 
       99 
90 
     | 
    
         
             
            ) -> Optional[int]:
         
     | 
| 
         @@ -367,7 +358,9 @@ def find_pairs( 
     | 
|
| 
       367 
358 
     | 
    
         
             
                            Residue(residue_i.label, residue_i.auth),
         
     | 
| 
       368 
359 
     | 
    
         
             
                            Residue(residue_j.label, residue_j.auth),
         
     | 
| 
       369 
360 
     | 
    
         
             
                            lw,
         
     | 
| 
       370 
     | 
    
         
            -
                             
     | 
| 
      
 361 
     | 
    
         
            +
                            Saenger.from_leontis_westhof(
         
     | 
| 
      
 362 
     | 
    
         
            +
                                residue_i.one_letter_name, residue_j.one_letter_name, lw
         
     | 
| 
      
 363 
     | 
    
         
            +
                            ),
         
     | 
| 
       371 
364 
     | 
    
         
             
                        )
         
     | 
| 
       372 
365 
     | 
    
         
             
                    )
         
     | 
| 
       373 
366 
     | 
    
         | 
| 
         @@ -483,7 +476,9 @@ def extract_base_interactions( 
     | 
|
| 
       483 
476 
     | 
    
         
             
            ) -> BaseInteractions:
         
     | 
| 
       484 
477 
     | 
    
         
             
                base_pairs, base_phosphate, base_ribose = find_pairs(tertiary_structure, model)
         
     | 
| 
       485 
478 
     | 
    
         
             
                stackings = find_stackings(tertiary_structure, model)
         
     | 
| 
       486 
     | 
    
         
            -
                return BaseInteractions( 
     | 
| 
      
 479 
     | 
    
         
            +
                return BaseInteractions.from_structure3d(
         
     | 
| 
      
 480 
     | 
    
         
            +
                    tertiary_structure, base_pairs, stackings, base_ribose, base_phosphate, []
         
     | 
| 
      
 481 
     | 
    
         
            +
                )
         
     | 
| 
       487 
482 
     | 
    
         | 
| 
       488 
483 
     | 
    
         | 
| 
       489 
484 
     | 
    
         
             
            def generate_pymol_script(mapping: Mapping2D3D, stems: List[Stem]) -> str:
         
     | 
| 
         @@ -688,91 +683,6 @@ def add_common_output_arguments(parser: argparse.ArgumentParser): 
     | 
|
| 
       688 
683 
     | 
    
         
             
                )
         
     | 
| 
       689 
684 
     | 
    
         | 
| 
       690 
685 
     | 
    
         | 
| 
       691 
     | 
    
         
            -
            def unify_structure_data(structure2d: Structure2D, mapping: Mapping2D3D) -> Structure2D:
         
     | 
| 
       692 
     | 
    
         
            -
                """
         
     | 
| 
       693 
     | 
    
         
            -
                Unify structure data by:
         
     | 
| 
       694 
     | 
    
         
            -
                1. Adding missing Saenger classifications to base pairs
         
     | 
| 
       695 
     | 
    
         
            -
                2. Filling in empty residue labels from Structure3D
         
     | 
| 
       696 
     | 
    
         
            -
                """
         
     | 
| 
       697 
     | 
    
         
            -
                # Create a mapping from residue to residue3d for label filling
         
     | 
| 
       698 
     | 
    
         
            -
                residue_to_residue3d = {}
         
     | 
| 
       699 
     | 
    
         
            -
                for residue3d in mapping.structure3d.residues:
         
     | 
| 
       700 
     | 
    
         
            -
                    residue_key = Residue(residue3d.label, residue3d.auth)
         
     | 
| 
       701 
     | 
    
         
            -
                    residue_to_residue3d[residue_key] = residue3d
         
     | 
| 
       702 
     | 
    
         
            -
             
     | 
| 
       703 
     | 
    
         
            -
                def fill_residue_label(residue: Residue) -> Residue:
         
     | 
| 
       704 
     | 
    
         
            -
                    """Fill empty label from Structure3D if available."""
         
     | 
| 
       705 
     | 
    
         
            -
                    if residue.label is not None:
         
     | 
| 
       706 
     | 
    
         
            -
                        return residue
         
     | 
| 
       707 
     | 
    
         
            -
             
     | 
| 
       708 
     | 
    
         
            -
                    # Try to find matching residue3d by auth
         
     | 
| 
       709 
     | 
    
         
            -
                    for residue3d in mapping.structure3d.residues:
         
     | 
| 
       710 
     | 
    
         
            -
                        if residue.auth == residue3d.auth:
         
     | 
| 
       711 
     | 
    
         
            -
                            return Residue(residue3d.label, residue.auth)
         
     | 
| 
       712 
     | 
    
         
            -
             
     | 
| 
       713 
     | 
    
         
            -
                    return residue
         
     | 
| 
       714 
     | 
    
         
            -
             
     | 
| 
       715 
     | 
    
         
            -
                # Process base pairs
         
     | 
| 
       716 
     | 
    
         
            -
                unified_base_pairs = []
         
     | 
| 
       717 
     | 
    
         
            -
                for base_pair in structure2d.base_pairs:
         
     | 
| 
       718 
     | 
    
         
            -
                    # Fill in missing labels
         
     | 
| 
       719 
     | 
    
         
            -
                    nt1 = fill_residue_label(base_pair.nt1)
         
     | 
| 
       720 
     | 
    
         
            -
                    nt2 = fill_residue_label(base_pair.nt2)
         
     | 
| 
       721 
     | 
    
         
            -
             
     | 
| 
       722 
     | 
    
         
            -
                    # Detect missing Saenger classification
         
     | 
| 
       723 
     | 
    
         
            -
                    saenger = base_pair.saenger
         
     | 
| 
       724 
     | 
    
         
            -
                    if saenger is None:
         
     | 
| 
       725 
     | 
    
         
            -
                        # Find corresponding 3D residues for Saenger detection
         
     | 
| 
       726 
     | 
    
         
            -
                        residue3d_1 = residue_to_residue3d.get(Residue(nt1.label, nt1.auth))
         
     | 
| 
       727 
     | 
    
         
            -
                        residue3d_2 = residue_to_residue3d.get(Residue(nt2.label, nt2.auth))
         
     | 
| 
       728 
     | 
    
         
            -
             
     | 
| 
       729 
     | 
    
         
            -
                        if residue3d_1 is not None and residue3d_2 is not None:
         
     | 
| 
       730 
     | 
    
         
            -
                            saenger = detect_saenger(residue3d_1, residue3d_2, base_pair.lw)
         
     | 
| 
       731 
     | 
    
         
            -
             
     | 
| 
       732 
     | 
    
         
            -
                    unified_base_pairs.append(BasePair(nt1, nt2, base_pair.lw, saenger))
         
     | 
| 
       733 
     | 
    
         
            -
             
     | 
| 
       734 
     | 
    
         
            -
                # Process other interaction types (fill labels only)
         
     | 
| 
       735 
     | 
    
         
            -
                unified_stackings = []
         
     | 
| 
       736 
     | 
    
         
            -
                for stacking in structure2d.stackings:
         
     | 
| 
       737 
     | 
    
         
            -
                    nt1 = fill_residue_label(stacking.nt1)
         
     | 
| 
       738 
     | 
    
         
            -
                    nt2 = fill_residue_label(stacking.nt2)
         
     | 
| 
       739 
     | 
    
         
            -
                    unified_stackings.append(Stacking(nt1, nt2, stacking.topology))
         
     | 
| 
       740 
     | 
    
         
            -
             
     | 
| 
       741 
     | 
    
         
            -
                unified_base_ribose = []
         
     | 
| 
       742 
     | 
    
         
            -
                for base_ribose in structure2d.base_ribose_interactions:
         
     | 
| 
       743 
     | 
    
         
            -
                    nt1 = fill_residue_label(base_ribose.nt1)
         
     | 
| 
       744 
     | 
    
         
            -
                    nt2 = fill_residue_label(base_ribose.nt2)
         
     | 
| 
       745 
     | 
    
         
            -
                    unified_base_ribose.append(BaseRibose(nt1, nt2, base_ribose.br))
         
     | 
| 
       746 
     | 
    
         
            -
             
     | 
| 
       747 
     | 
    
         
            -
                unified_base_phosphate = []
         
     | 
| 
       748 
     | 
    
         
            -
                for base_phosphate in structure2d.base_phosphate_interactions:
         
     | 
| 
       749 
     | 
    
         
            -
                    nt1 = fill_residue_label(base_phosphate.nt1)
         
     | 
| 
       750 
     | 
    
         
            -
                    nt2 = fill_residue_label(base_phosphate.nt2)
         
     | 
| 
       751 
     | 
    
         
            -
                    unified_base_phosphate.append(BasePhosphate(nt1, nt2, base_phosphate.bph))
         
     | 
| 
       752 
     | 
    
         
            -
             
     | 
| 
       753 
     | 
    
         
            -
                unified_other = []
         
     | 
| 
       754 
     | 
    
         
            -
                for other in structure2d.other_interactions:
         
     | 
| 
       755 
     | 
    
         
            -
                    nt1 = fill_residue_label(other.nt1)
         
     | 
| 
       756 
     | 
    
         
            -
                    nt2 = fill_residue_label(other.nt2)
         
     | 
| 
       757 
     | 
    
         
            -
                    unified_other.append(OtherInteraction(nt1, nt2))
         
     | 
| 
       758 
     | 
    
         
            -
             
     | 
| 
       759 
     | 
    
         
            -
                # Create new Structure2D with unified data
         
     | 
| 
       760 
     | 
    
         
            -
                unified_base_interactions = BaseInteractions(
         
     | 
| 
       761 
     | 
    
         
            -
                    unified_base_pairs,
         
     | 
| 
       762 
     | 
    
         
            -
                    unified_stackings,
         
     | 
| 
       763 
     | 
    
         
            -
                    unified_base_ribose,
         
     | 
| 
       764 
     | 
    
         
            -
                    unified_base_phosphate,
         
     | 
| 
       765 
     | 
    
         
            -
                    unified_other,
         
     | 
| 
       766 
     | 
    
         
            -
                )
         
     | 
| 
       767 
     | 
    
         
            -
             
     | 
| 
       768 
     | 
    
         
            -
                # Recreate Structure2D with unified interactions
         
     | 
| 
       769 
     | 
    
         
            -
                unified_structure2d, _ = mapping.structure3d.extract_secondary_structure(
         
     | 
| 
       770 
     | 
    
         
            -
                    unified_base_interactions, False
         
     | 
| 
       771 
     | 
    
         
            -
                )
         
     | 
| 
       772 
     | 
    
         
            -
             
     | 
| 
       773 
     | 
    
         
            -
                return unified_structure2d
         
     | 
| 
       774 
     | 
    
         
            -
             
     | 
| 
       775 
     | 
    
         
            -
             
     | 
| 
       776 
686 
     | 
    
         
             
            def handle_output_arguments(
         
     | 
| 
       777 
687 
     | 
    
         
             
                args: argparse.Namespace,
         
     | 
| 
       778 
688 
     | 
    
         
             
                structure2d: Structure2D,
         
     | 
| 
         @@ -780,34 +690,31 @@ def handle_output_arguments( 
     | 
|
| 
       780 
690 
     | 
    
         
             
                input_filename: str,
         
     | 
| 
       781 
691 
     | 
    
         
             
            ):
         
     | 
| 
       782 
692 
     | 
    
         
             
                """Handles writing output based on provided arguments."""
         
     | 
| 
       783 
     | 
    
         
            -
                # Unify the structure data before processing outputs
         
     | 
| 
       784 
     | 
    
         
            -
                unified_structure2d = unify_structure_data(structure2d, mapping)
         
     | 
| 
       785 
     | 
    
         
            -
             
     | 
| 
       786 
693 
     | 
    
         
             
                input_basename = os.path.basename(input_filename)
         
     | 
| 
       787 
694 
     | 
    
         
             
                if args.csv:
         
     | 
| 
       788 
     | 
    
         
            -
                    write_csv(args.csv,  
     | 
| 
      
 695 
     | 
    
         
            +
                    write_csv(args.csv, structure2d)
         
     | 
| 
       789 
696 
     | 
    
         | 
| 
       790 
697 
     | 
    
         
             
                if args.json:
         
     | 
| 
       791 
     | 
    
         
            -
                    write_json(args.json,  
     | 
| 
      
 698 
     | 
    
         
            +
                    write_json(args.json, structure2d)
         
     | 
| 
       792 
699 
     | 
    
         | 
| 
       793 
700 
     | 
    
         
             
                if args.bpseq:
         
     | 
| 
       794 
     | 
    
         
            -
                    write_bpseq(args.bpseq,  
     | 
| 
      
 701 
     | 
    
         
            +
                    write_bpseq(args.bpseq, structure2d.bpseq)
         
     | 
| 
       795 
702 
     | 
    
         | 
| 
       796 
703 
     | 
    
         
             
                if args.extended:
         
     | 
| 
       797 
     | 
    
         
            -
                    print( 
     | 
| 
      
 704 
     | 
    
         
            +
                    print(structure2d.extended_dot_bracket)
         
     | 
| 
       798 
705 
     | 
    
         
             
                else:
         
     | 
| 
       799 
     | 
    
         
            -
                    print( 
     | 
| 
      
 706 
     | 
    
         
            +
                    print(structure2d.dot_bracket)
         
     | 
| 
       800 
707 
     | 
    
         | 
| 
       801 
708 
     | 
    
         
             
                if args.dot:
         
     | 
| 
       802 
     | 
    
         
            -
                    print(BpSeq.from_string( 
     | 
| 
      
 709 
     | 
    
         
            +
                    print(BpSeq.from_string(structure2d.bpseq).graphviz)
         
     | 
| 
       803 
710 
     | 
    
         | 
| 
       804 
711 
     | 
    
         
             
                if args.pml:
         
     | 
| 
       805 
     | 
    
         
            -
                    pml_script = generate_pymol_script(mapping,  
     | 
| 
      
 712 
     | 
    
         
            +
                    pml_script = generate_pymol_script(mapping, structure2d.stems)
         
     | 
| 
       806 
713 
     | 
    
         
             
                    with open(args.pml, "w") as f:
         
     | 
| 
       807 
714 
     | 
    
         
             
                        f.write(pml_script)
         
     | 
| 
       808 
715 
     | 
    
         | 
| 
       809 
716 
     | 
    
         
             
                if args.inter_stem_csv:
         
     | 
| 
       810 
     | 
    
         
            -
                    if  
     | 
| 
      
 717 
     | 
    
         
            +
                    if structure2d.inter_stem_parameters:
         
     | 
| 
       811 
718 
     | 
    
         
             
                        # Convert list of dataclasses to list of dicts
         
     | 
| 
       812 
719 
     | 
    
         
             
                        params_list = [
         
     | 
| 
       813 
720 
     | 
    
         
             
                            {
         
     | 
| 
         @@ -820,7 +727,7 @@ def handle_output_arguments( 
     | 
|
| 
       820 
727 
     | 
    
         
             
                                "min_endpoint_distance_pdf": p.min_endpoint_distance_pdf,
         
     | 
| 
       821 
728 
     | 
    
         
             
                                "coaxial_probability": p.coaxial_probability,
         
     | 
| 
       822 
729 
     | 
    
         
             
                            }
         
     | 
| 
       823 
     | 
    
         
            -
                            for p in  
     | 
| 
      
 730 
     | 
    
         
            +
                            for p in structure2d.interStemParameters
         
     | 
| 
       824 
731 
     | 
    
         
             
                        ]
         
     | 
| 
       825 
732 
     | 
    
         
             
                        df = pd.DataFrame(params_list)
         
     | 
| 
       826 
733 
     | 
    
         
             
                        df["input_basename"] = input_basename
         
     | 
| 
         @@ -838,9 +745,9 @@ def handle_output_arguments( 
     | 
|
| 
       838 
745 
     | 
    
         
             
                        # pd.DataFrame(columns=['input_basename', 'stem1_idx', ...]).to_csv(args.inter_stem_csv, index=False)
         
     | 
| 
       839 
746 
     | 
    
         | 
| 
       840 
747 
     | 
    
         
             
                if args.stems_csv:
         
     | 
| 
       841 
     | 
    
         
            -
                    if  
     | 
| 
      
 748 
     | 
    
         
            +
                    if structure2d.stems:
         
     | 
| 
       842 
749 
     | 
    
         
             
                        stems_data = []
         
     | 
| 
       843 
     | 
    
         
            -
                        for i, stem in enumerate( 
     | 
| 
      
 750 
     | 
    
         
            +
                        for i, stem in enumerate(structure2d.stems):
         
     | 
| 
       844 
751 
     | 
    
         
             
                            try:
         
     | 
| 
       845 
752 
     | 
    
         
             
                                res5p_first = mapping.bpseq_index_to_residue_map.get(
         
     | 
| 
       846 
753 
     | 
    
         
             
                                    stem.strand5p.first
         
     | 
    
        rnapolis/common.py
    CHANGED
    
    | 
         @@ -5,7 +5,7 @@ import re 
     | 
|
| 
       5 
5 
     | 
    
         
             
            import string
         
     | 
| 
       6 
6 
     | 
    
         
             
            from collections import defaultdict
         
     | 
| 
       7 
7 
     | 
    
         
             
            from collections.abc import Sequence
         
     | 
| 
       8 
     | 
    
         
            -
            from dataclasses import dataclass
         
     | 
| 
      
 8 
     | 
    
         
            +
            from dataclasses import InitVar, dataclass
         
     | 
| 
       9 
9 
     | 
    
         
             
            from enum import Enum
         
     | 
| 
       10 
10 
     | 
    
         
             
            from functools import cache, cached_property, total_ordering
         
     | 
| 
       11 
11 
     | 
    
         
             
            from typing import Dict, List, Optional, Tuple
         
     | 
| 
         @@ -152,6 +152,18 @@ class Saenger(Enum): 
     | 
|
| 
       152 
152 
     | 
    
         
             
                        ("TG", "cWW"): "XXVIII",
         
     | 
| 
       153 
153 
     | 
    
         
             
                    }
         
     | 
| 
       154 
154 
     | 
    
         | 
| 
      
 155 
     | 
    
         
            +
                @classmethod
         
     | 
| 
      
 156 
     | 
    
         
            +
                def from_leontis_westhof(
         
     | 
| 
      
 157 
     | 
    
         
            +
                    cls,
         
     | 
| 
      
 158 
     | 
    
         
            +
                    residue_i_one_letter_name: str,
         
     | 
| 
      
 159 
     | 
    
         
            +
                    residue_j_one_letter_name: str,
         
     | 
| 
      
 160 
     | 
    
         
            +
                    lw: LeontisWesthof,
         
     | 
| 
      
 161 
     | 
    
         
            +
                ) -> Optional["Saenger"]:
         
     | 
| 
      
 162 
     | 
    
         
            +
                    key = (f"{residue_i_one_letter_name}{residue_j_one_letter_name}", lw.value)
         
     | 
| 
      
 163 
     | 
    
         
            +
                    if key in Saenger.table():
         
     | 
| 
      
 164 
     | 
    
         
            +
                        return Saenger[Saenger.table()[key]]
         
     | 
| 
      
 165 
     | 
    
         
            +
                    return None
         
     | 
| 
      
 166 
     | 
    
         
            +
             
     | 
| 
       155 
167 
     | 
    
         
             
                @property
         
     | 
| 
       156 
168 
     | 
    
         
             
                def is_canonical(self) -> bool:
         
     | 
| 
       157 
169 
     | 
    
         
             
                    return self == Saenger.XIX or self == Saenger.XX or self == Saenger.XXVIII
         
     | 
| 
         @@ -1062,6 +1074,91 @@ class BaseInteractions: 
     | 
|
| 
       1062 
1074 
     | 
    
         
             
                base_phosphate_interactions: List[BasePhosphate]
         
     | 
| 
       1063 
1075 
     | 
    
         
             
                other_interactions: List[OtherInteraction]
         
     | 
| 
       1064 
1076 
     | 
    
         | 
| 
      
 1077 
     | 
    
         
            +
                @classmethod
         
     | 
| 
      
 1078 
     | 
    
         
            +
                def from_structure3d(
         
     | 
| 
      
 1079 
     | 
    
         
            +
                    cls,
         
     | 
| 
      
 1080 
     | 
    
         
            +
                    structure3d: "Structure3D",
         
     | 
| 
      
 1081 
     | 
    
         
            +
                    base_pairs: List[BasePair],
         
     | 
| 
      
 1082 
     | 
    
         
            +
                    stackings: List[Stacking],
         
     | 
| 
      
 1083 
     | 
    
         
            +
                    base_ribose_interactions: List[BaseRibose],
         
     | 
| 
      
 1084 
     | 
    
         
            +
                    base_phosphate_interactions: List[BasePhosphate],
         
     | 
| 
      
 1085 
     | 
    
         
            +
                    other_interactions: List[OtherInteraction],
         
     | 
| 
      
 1086 
     | 
    
         
            +
                ) -> "BaseInteractions":
         
     | 
| 
      
 1087 
     | 
    
         
            +
                    auth2residue3d = {}
         
     | 
| 
      
 1088 
     | 
    
         
            +
                    auth2label = {}
         
     | 
| 
      
 1089 
     | 
    
         
            +
                    label2auth = {}
         
     | 
| 
      
 1090 
     | 
    
         
            +
             
     | 
| 
      
 1091 
     | 
    
         
            +
                    for residue3d in structure3d.residues:
         
     | 
| 
      
 1092 
     | 
    
         
            +
                        auth2residue3d[residue3d.auth] = residue3d
         
     | 
| 
      
 1093 
     | 
    
         
            +
                        auth2label[residue3d.auth] = residue3d.label
         
     | 
| 
      
 1094 
     | 
    
         
            +
                        label2auth[residue3d.label] = residue3d.auth
         
     | 
| 
      
 1095 
     | 
    
         
            +
             
     | 
| 
      
 1096 
     | 
    
         
            +
                    def unify_nt(nt: Residue) -> Residue:
         
     | 
| 
      
 1097 
     | 
    
         
            +
                        if nt.auth is not None and nt.label is not None:
         
     | 
| 
      
 1098 
     | 
    
         
            +
                            return nt
         
     | 
| 
      
 1099 
     | 
    
         
            +
                        if nt.auth is not None:
         
     | 
| 
      
 1100 
     | 
    
         
            +
                            return Residue(label=auth2label.get(nt.auth, None), auth=nt.auth)
         
     | 
| 
      
 1101 
     | 
    
         
            +
                        if nt.label is not None:
         
     | 
| 
      
 1102 
     | 
    
         
            +
                            return Residue(label=nt.label, auth=label2auth.get(nt.label, None))
         
     | 
| 
      
 1103 
     | 
    
         
            +
                        return nt
         
     | 
| 
      
 1104 
     | 
    
         
            +
             
     | 
| 
      
 1105 
     | 
    
         
            +
                    base_pairs_new = []
         
     | 
| 
      
 1106 
     | 
    
         
            +
                    for base_pair in base_pairs:
         
     | 
| 
      
 1107 
     | 
    
         
            +
                        nt1 = unify_nt(base_pair.nt1)
         
     | 
| 
      
 1108 
     | 
    
         
            +
                        nt2 = unify_nt(base_pair.nt2)
         
     | 
| 
      
 1109 
     | 
    
         
            +
                        saenger = base_pair.saenger or Saenger.from_leontis_westhof(
         
     | 
| 
      
 1110 
     | 
    
         
            +
                            auth2residue3d[nt1.auth].one_letter_name,
         
     | 
| 
      
 1111 
     | 
    
         
            +
                            auth2residue3d[nt2.auth].one_letter_name,
         
     | 
| 
      
 1112 
     | 
    
         
            +
                            base_pair.lw,
         
     | 
| 
      
 1113 
     | 
    
         
            +
                        )
         
     | 
| 
      
 1114 
     | 
    
         
            +
                        if (
         
     | 
| 
      
 1115 
     | 
    
         
            +
                            nt1 != base_pair.nt1
         
     | 
| 
      
 1116 
     | 
    
         
            +
                            or nt2 != base_pair.nt2
         
     | 
| 
      
 1117 
     | 
    
         
            +
                            or saenger != base_pair.saenger
         
     | 
| 
      
 1118 
     | 
    
         
            +
                        ):
         
     | 
| 
      
 1119 
     | 
    
         
            +
                            base_pair = BasePair(nt1=nt1, nt2=nt2, lw=base_pair.lw, saenger=saenger)
         
     | 
| 
      
 1120 
     | 
    
         
            +
                        base_pairs_new.append(base_pair)
         
     | 
| 
      
 1121 
     | 
    
         
            +
             
     | 
| 
      
 1122 
     | 
    
         
            +
                    stackings_new = []
         
     | 
| 
      
 1123 
     | 
    
         
            +
                    for stacking in stackings:
         
     | 
| 
      
 1124 
     | 
    
         
            +
                        nt1 = unify_nt(stacking.nt1)
         
     | 
| 
      
 1125 
     | 
    
         
            +
                        nt2 = unify_nt(stacking.nt2)
         
     | 
| 
      
 1126 
     | 
    
         
            +
                        if nt1 != stacking.nt1 or nt2 != stacking.nt2:
         
     | 
| 
      
 1127 
     | 
    
         
            +
                            stacking = Stacking(nt1=nt1, nt2=nt2, topology=stacking.topology)
         
     | 
| 
      
 1128 
     | 
    
         
            +
                        stackings_new.append(stacking)
         
     | 
| 
      
 1129 
     | 
    
         
            +
             
     | 
| 
      
 1130 
     | 
    
         
            +
                    base_ribose_interactions_new = []
         
     | 
| 
      
 1131 
     | 
    
         
            +
                    for base_ribose in base_ribose_interactions:
         
     | 
| 
      
 1132 
     | 
    
         
            +
                        nt1 = unify_nt(base_ribose.nt1)
         
     | 
| 
      
 1133 
     | 
    
         
            +
                        nt2 = unify_nt(base_ribose.nt2)
         
     | 
| 
      
 1134 
     | 
    
         
            +
                        if nt1 != base_ribose.nt1 or nt2 != base_ribose.nt2:
         
     | 
| 
      
 1135 
     | 
    
         
            +
                            base_ribose = BaseRibose(nt1=nt1, nt2=nt2, br=base_ribose.br)
         
     | 
| 
      
 1136 
     | 
    
         
            +
                        base_ribose_interactions_new.append(base_ribose)
         
     | 
| 
      
 1137 
     | 
    
         
            +
             
     | 
| 
      
 1138 
     | 
    
         
            +
                    base_phosphate_interactions_new = []
         
     | 
| 
      
 1139 
     | 
    
         
            +
                    for base_phosphate in base_phosphate_interactions:
         
     | 
| 
      
 1140 
     | 
    
         
            +
                        nt1 = unify_nt(base_phosphate.nt1)
         
     | 
| 
      
 1141 
     | 
    
         
            +
                        nt2 = unify_nt(base_phosphate.nt2)
         
     | 
| 
      
 1142 
     | 
    
         
            +
                        if nt1 != base_phosphate.nt1 or nt2 != base_phosphate.nt2:
         
     | 
| 
      
 1143 
     | 
    
         
            +
                            base_phosphate = BasePhosphate(nt1=nt1, nt2=nt2, bph=base_phosphate.bph)
         
     | 
| 
      
 1144 
     | 
    
         
            +
                        base_phosphate_interactions_new.append(base_phosphate)
         
     | 
| 
      
 1145 
     | 
    
         
            +
             
     | 
| 
      
 1146 
     | 
    
         
            +
                    other_interactions_new = []
         
     | 
| 
      
 1147 
     | 
    
         
            +
                    for other_interaction in other_interactions:
         
     | 
| 
      
 1148 
     | 
    
         
            +
                        nt1 = unify_nt(other_interaction.nt1)
         
     | 
| 
      
 1149 
     | 
    
         
            +
                        nt2 = unify_nt(other_interaction.nt2)
         
     | 
| 
      
 1150 
     | 
    
         
            +
                        if nt1 != other_interaction.nt1 or nt2 != other_interaction.nt2:
         
     | 
| 
      
 1151 
     | 
    
         
            +
                            other_interaction = OtherInteraction(nt1=nt1, nt2=nt2)
         
     | 
| 
      
 1152 
     | 
    
         
            +
                        other_interactions_new.append(other_interaction)
         
     | 
| 
      
 1153 
     | 
    
         
            +
             
     | 
| 
      
 1154 
     | 
    
         
            +
                    return cls(
         
     | 
| 
      
 1155 
     | 
    
         
            +
                        base_pairs=base_pairs_new,
         
     | 
| 
      
 1156 
     | 
    
         
            +
                        stackings=stackings_new,
         
     | 
| 
      
 1157 
     | 
    
         
            +
                        base_ribose_interactions=base_ribose_interactions_new,
         
     | 
| 
      
 1158 
     | 
    
         
            +
                        base_phosphate_interactions=base_phosphate_interactions_new,
         
     | 
| 
      
 1159 
     | 
    
         
            +
                        other_interactions=other_interactions_new,
         
     | 
| 
      
 1160 
     | 
    
         
            +
                    )
         
     | 
| 
      
 1161 
     | 
    
         
            +
             
     | 
| 
       1065 
1162 
     | 
    
         | 
| 
       1066 
1163 
     | 
    
         
             
            @dataclass(frozen=True, order=True)
         
     | 
| 
       1067 
1164 
     | 
    
         
             
            class InterStemParameters:
         
     | 
| 
         @@ -1,8 +1,8 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
            rnapolis/adapter.py,sha256= 
     | 
| 
      
 1 
     | 
    
         
            +
            rnapolis/adapter.py,sha256=6hJTweIqUXH8CEGvi8oupFzk5etkIt8Q2bqRvgsqako,62169
         
     | 
| 
       2 
2 
     | 
    
         
             
            rnapolis/aligner.py,sha256=o7rQyjAZ3n4VXcnSPY3HVB8nLNRkVbl552O3NVh0mfg,3429
         
     | 
| 
       3 
     | 
    
         
            -
            rnapolis/annotator.py,sha256= 
     | 
| 
      
 3 
     | 
    
         
            +
            rnapolis/annotator.py,sha256=HA2hfEUXdmBElObqRlASAB1FgkysjiHgwMTjEhsDiDE,30277
         
     | 
| 
       4 
4 
     | 
    
         
             
            rnapolis/clashfinder.py,sha256=AC9_tIx7QIk57sELq_aKfU1u3UMOXbgcccQeGHhMR6c,8517
         
     | 
| 
       5 
     | 
    
         
            -
            rnapolis/common.py,sha256= 
     | 
| 
      
 5 
     | 
    
         
            +
            rnapolis/common.py,sha256=hamlW892ZF5A0dSWsl7cOCZqOpbVQMgXjVPYDFzk3pE,36347
         
     | 
| 
       6 
6 
     | 
    
         
             
            rnapolis/component_A.csv,sha256=koirS-AwUZwoYGItT8yn3wS6Idvmh2FANfTQcOS_xh8,2897
         
     | 
| 
       7 
7 
     | 
    
         
             
            rnapolis/component_C.csv,sha256=NtvsAu_YrUgTjzZm3j4poW4IZ99x3dPARB09XVIiMCc,2803
         
     | 
| 
       8 
8 
     | 
    
         
             
            rnapolis/component_G.csv,sha256=Z5wl8OnHRyx4XhTyBiWgRZiEvmZXhoxtVRH8bn6Vxf0,2898
         
     | 
| 
         @@ -22,9 +22,9 @@ rnapolis/tertiary_v2.py,sha256=SgijTv0bPqMJwsMqyQk0O8QAnS2Ozk45vk8igxt9hRs,38001 
     | 
|
| 
       22 
22 
     | 
    
         
             
            rnapolis/transformer.py,sha256=aC0nBmHHJf5TyLvBIV57Jj3tlwpvHbPo347opfAOlQA,3844
         
     | 
| 
       23 
23 
     | 
    
         
             
            rnapolis/unifier.py,sha256=2ge7IB9FdRgzSAiVD39U_ciwtdDJ2fGzf8mUIudbrqY,5820
         
     | 
| 
       24 
24 
     | 
    
         
             
            rnapolis/util.py,sha256=IdquFO3PV1_KDqodjupzm0Rqvgy0CeSzxGHaGEHYXVU,543
         
     | 
| 
       25 
     | 
    
         
            -
            rnapolis-0.10. 
     | 
| 
       26 
     | 
    
         
            -
            rnapolis-0.10. 
     | 
| 
       27 
     | 
    
         
            -
            rnapolis-0.10. 
     | 
| 
       28 
     | 
    
         
            -
            rnapolis-0.10. 
     | 
| 
       29 
     | 
    
         
            -
            rnapolis-0.10. 
     | 
| 
       30 
     | 
    
         
            -
            rnapolis-0.10. 
     | 
| 
      
 25 
     | 
    
         
            +
            rnapolis-0.10.6.dist-info/licenses/LICENSE,sha256=ZGRu12MzCgbYA-Lt8MyBlmjvPZh7xfiD5u5wBx0enq4,1066
         
     | 
| 
      
 26 
     | 
    
         
            +
            rnapolis-0.10.6.dist-info/METADATA,sha256=Q2OY_Y3PZgVNaob7Xk8vruYNZ13HyFfdiRD7giJqJ_I,54611
         
     | 
| 
      
 27 
     | 
    
         
            +
            rnapolis-0.10.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
         
     | 
| 
      
 28 
     | 
    
         
            +
            rnapolis-0.10.6.dist-info/entry_points.txt,sha256=MZMWnYBUYnis-zWDmFfuA5yXtU3W5YdQrm5HA5LrkeM,474
         
     | 
| 
      
 29 
     | 
    
         
            +
            rnapolis-0.10.6.dist-info/top_level.txt,sha256=LcO18koxZcWoJ21KDRRRo_tyIbmXL5z61dPitZpy8yc,9
         
     | 
| 
      
 30 
     | 
    
         
            +
            rnapolis-0.10.6.dist-info/RECORD,,
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     |