JAL-3053
authorBen Soares <b.soares@dundee.ac.uk>
Thu, 2 Aug 2018 16:30:15 +0000 (17:30 +0100)
committerBen Soares <b.soares@dundee.ac.uk>
Thu, 2 Aug 2018 16:30:15 +0000 (17:30 +0100)
JAL-3061
two bugs with a similar theme (RNA secondary structure annotations)

JAL-3053
This fixes the reading in of Vienna extended dot-bracket notation for
RNA secondary structure annotations (which includes ALPHA/alpha braces).

To avoid (almost all) ambiguity with a protein secondary structure
annotation (Es and Hs), there's a whole-annotation regex match to ensure
some other RNA-based extended dot-bracket feature is present, and then
the reading in can assume an RNA feature set.

Test StockholmFileTest.stockholmFileRnaSSAlphaChars() compares the test
file examples/rna_ss_test.stk with a hard-coded Jalview equivalent. The
Annotation rnasecstr pairing is checked. I've added a sort Comparator
for SequenceFeature to help ensure that check is correct.
--
JAL-3061
There is also a fix for the RNA Secondary Structure annotation output
that was preserving spaces in the output. This is not valid output in
this case, and so a '.' is used instead.

No test for this yet, will add later.

examples/rna_ss_test.stk
src/jalview/datamodel/AlignmentAnnotation.java
src/jalview/datamodel/SequenceFeature.java
src/jalview/io/StockholmFile.java
test/jalview/io/StockholmFileTest.java

index e716e0c..429612e 100644 (file)
@@ -1,6 +1,6 @@
 # STOCKHOLM 1.0
 #=GF ID RNA.SS.TEST
 #=GF TP RNA;
-Test.sequence         AAAAAA
-#=GC SS_cons          (EHhe)
+Test.sequence         GUACAAAAAAAAAA
+#=GC SS_cons          <(EHBheb(E)e)>
 //
index f84b18d..ee9389c 100755 (executable)
@@ -25,6 +25,7 @@ import jalview.analysis.SecStrConsensus.SimpleBP;
 import jalview.analysis.WUSSParseException;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -164,6 +165,64 @@ public class AlignmentAnnotation
   }
 
   /**
+   * Get the RNA Secondary Structure SequenceFeature Array if present
+   */
+  public SequenceFeature[] getRnaSecondaryStructure()
+  {
+    return this._rnasecstr;
+  }
+
+  /**
+   * Check the RNA Secondary Structure is equivalent to one in given
+   * AlignmentAnnotation param
+   */
+  public boolean rnaSecondaryStructureEquivalent(AlignmentAnnotation that)
+  {
+    return rnaSecondaryStructureEquivalent(that, true);
+  }
+
+  public boolean rnaSecondaryStructureEquivalent(AlignmentAnnotation that, boolean compareType)
+  {
+    SequenceFeature[] thisSfArray = this.getRnaSecondaryStructure();
+    SequenceFeature[] thatSfArray = that.getRnaSecondaryStructure();
+    if (thisSfArray == null || thatSfArray == null)
+    {
+      return thisSfArray == null && thatSfArray == null;
+    }
+    if (thisSfArray.length != thatSfArray.length)
+    {
+      return false;
+    }
+    Arrays.sort(thisSfArray, new SFSortByEnd()); // probably already sorted
+                                                   // like this
+    Arrays.sort(thatSfArray, new SFSortByEnd()); // probably already sorted
+                                                   // like this
+    for (int i=0; i < thisSfArray.length; i++) {
+      SequenceFeature thisSf = thisSfArray[i];
+      SequenceFeature thatSf = thatSfArray[i];
+      if (compareType) {
+        if (thisSf.getType() == null || thatSf.getType() == null) {
+          if (thisSf.getType() == null && thatSf.getType() == null) {
+            continue;
+          } else {
+            return false;
+          }
+        }
+        if (! thisSf.getType().equals(thatSf.getType())) {
+          return false;
+        }
+      }
+      if (!(thisSf.getBegin() == thatSf.getBegin()
+              && thisSf.getEnd() == thatSf.getEnd()))
+      {
+        return false;
+      }
+    }
+    return true;
+
+  }
+
+  /**
    * map of positions in the associated annotation
    */
   private Map<Integer, Annotation> sequenceMapping;
@@ -321,10 +380,12 @@ public class AlignmentAnnotation
                 || annotations[i].secondaryStructure == 'B'
                 || annotations[i].secondaryStructure == 'C'
                 || annotations[i].secondaryStructure == 'D'
-                // || annotations[i].secondaryStructure == 'E'
+                // || annotations[i].secondaryStructure == 'E' // ambiguous on
+                // its own -- already checked above
                 || annotations[i].secondaryStructure == 'F'
                 || annotations[i].secondaryStructure == 'G'
-                // || annotations[i].secondaryStructure == 'H'
+                // || annotations[i].secondaryStructure == 'H' // ambiguous on
+                // its own -- already checked above
                 || annotations[i].secondaryStructure == 'I'
                 || annotations[i].secondaryStructure == 'J'
                 || annotations[i].secondaryStructure == 'K'
@@ -1652,4 +1713,5 @@ public class AlignmentAnnotation
     }
     return aa;
   }
+
 }
index 9c4087e..f17bd33 100755 (executable)
@@ -22,6 +22,7 @@ package jalview.datamodel;
 
 import jalview.datamodel.features.FeatureLocationI;
 
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Map.Entry;
@@ -157,7 +158,7 @@ public class SequenceFeature implements FeatureLocationI
 
     if (sf.otherDetails != null)
     {
-      otherDetails = new HashMap<String, Object>();
+      otherDetails = new HashMap<>();
       for (Entry<String, Object> entry : sf.otherDetails.entrySet())
       {
         otherDetails.put(entry.getKey(), entry.getValue());
@@ -165,7 +166,7 @@ public class SequenceFeature implements FeatureLocationI
     }
     if (sf.links != null && sf.links.size() > 0)
     {
-      links = new Vector<String>();
+      links = new Vector<>();
       for (int i = 0, iSize = sf.links.size(); i < iSize; i++)
       {
         links.addElement(sf.links.elementAt(i));
@@ -332,7 +333,7 @@ public class SequenceFeature implements FeatureLocationI
   {
     if (links == null)
     {
-      links = new Vector<String>();
+      links = new Vector<>();
     }
 
     if (!links.contains(labelLink))
@@ -394,7 +395,7 @@ public class SequenceFeature implements FeatureLocationI
     {
       if (otherDetails == null)
       {
-        otherDetails = new HashMap<String, Object>();
+        otherDetails = new HashMap<>();
       }
 
       otherDetails.put(key, value);
@@ -536,3 +537,21 @@ public class SequenceFeature implements FeatureLocationI
     return begin == 0 && end == 0;
   }
 }
+
+class SFSortByEnd implements Comparator<SequenceFeature>
+{
+  @Override
+  public int compare(SequenceFeature a, SequenceFeature b)
+  {
+    return a.getEnd() - b.getEnd();
+  }
+}
+
+class SFSortByBegin implements Comparator<SequenceFeature>
+{
+  @Override
+  public int compare(SequenceFeature a, SequenceFeature b)
+  {
+    return a.getBegin() - b.getBegin();
+  }
+}
index 71e369e..606540f 100644 (file)
@@ -83,7 +83,13 @@ public class StockholmFile extends AlignFile
   public static final Regex DETECT_BRACKETS = new Regex(
           "(<|>|\\[|\\]|\\(|\\)|\\{|\\})");
 
-  public static final String RNASS_BRACKETS = "<>[]() {}AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz";
+  // WUSS extended symbols. Avoid ambiguity with protein SS annotations by using NOT_RNASS first.
+  public static final String RNASS_BRACKETS = "<>[](){}AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz";
+
+  // use the following regex to decide an annotations (whole) line is NOT an RNA
+  // SS (it contains only E,H,e,h and other non-brace/non-alpha chars)
+  private static final Regex NOT_RNASS = new Regex(
+          "^[^<>[\\](){}A-DF-Za-df-z]*$");
 
   StringBuffer out; // output buffer
 
@@ -199,7 +205,7 @@ public class StockholmFile extends AlignFile
     String version;
     // String id;
     Hashtable seqAnn = new Hashtable(); // Sequence related annotations
-    LinkedHashMap<String, String> seqs = new LinkedHashMap<>();
+    LinkedHashMap<String, String> seqs = new LinkedHashMap<String, String>();
     Regex p, r, rend, s, x;
     // Temporary line for processing RNA annotation
     // String RNAannot = "";
@@ -660,7 +666,7 @@ public class StockholmFile extends AlignFile
               strucAnn = new Hashtable();
             }
 
-            Vector<AlignmentAnnotation> newStruc = new Vector<>();
+            Vector<AlignmentAnnotation> newStruc = new Vector<AlignmentAnnotation>();
             parseAnnotationRow(newStruc, type, ns);
             for (AlignmentAnnotation alan : newStruc)
             {
@@ -712,7 +718,7 @@ public class StockholmFile extends AlignFile
   private void guessDatabaseFor(Sequence seqO, String dbr, String dbsource)
   {
     DBRefEntry dbrf = null;
-    List<DBRefEntry> dbrs = new ArrayList<>();
+    List<DBRefEntry> dbrs = new ArrayList<DBRefEntry>();
     String seqdb = "Unknown", sdbac = "" + dbr;
     int st = -1, en = -1, p;
     if ((st = sdbac.indexOf("/")) > -1)
@@ -816,12 +822,6 @@ public class StockholmFile extends AlignFile
     // convert1 = OPEN_PAREN.replaceAll(annots);
     // convert2 = CLOSE_PAREN.replaceAll(convert1);
     // annots = convert2;
-    
-    // DEBUG
-    System.out.println(
-            "*** parseAnnotationRow called with\n      annotation='"
-                    + annotation + "'\n      label='" + label
-                    + "'\n      annots='" + annots + "'");
 
     String type = label;
     if (label.contains("_cons"))
@@ -837,7 +837,9 @@ public class StockholmFile extends AlignFile
     if (type.equalsIgnoreCase("secondary structure"))
     {
       ss = true;
-      isrnass = DETECT_BRACKETS.search(annots);
+      isrnass = !NOT_RNASS.search(annots); // sorry about the double negative
+                                           // here (it's easier for dealing with
+                                           // other non-alpha-non-brace chars)
     }
     if (type.equalsIgnoreCase("posterior probability"))
     {
@@ -1019,9 +1021,7 @@ public class StockholmFile extends AlignFile
 
           String key = type2id(alAnot[j].label);
           boolean isrna = alAnot[j].isValidStruc();
-          // bs debug
-          System.out.println("SEQUENCE " + i + "/" + s.length + " ISRNA="
-                  + isrna + ".");
+
           if (isrna)
           {
             // hardwire to secondary structure if there is RNA secondary
@@ -1043,9 +1043,6 @@ public class StockholmFile extends AlignFile
           {
             seq += outputCharacter(key, k, isrna, ann, s[i]);
           }
-          // bs debug
-          System.out.println("APPENDING SEQ: KEY=" + key + " ISRNA=" + isrna
-                  + ".\n" + "SEQ=" + seq + "\n");
           out.append(seq);
           out.append(newline);
         }
@@ -1054,8 +1051,6 @@ public class StockholmFile extends AlignFile
       out.append(new Format("%-" + maxid + "s")
               .form(printId(s[i], jvSuffix) + " "));
       out.append(s[i].getSequenceAsString());
-      // bs debug
-      System.out.println("ALSO APPENDING " + s[i].getSequenceAsString());
       out.append(newline);
       i++;
     }
@@ -1103,12 +1098,6 @@ public class StockholmFile extends AlignFile
         {
           seq += outputCharacter(key, j, isrna, aa.annotations, null);
         }
-        
-        // bs debug
-        System.out.println(
-                "PRINTING SEQ: KEY=" + key + " ISRNA=" + isrna + ".\n"
-                        + "SEQ=" + seq + "\n");
-        
         out.append(seq);
         out.append(newline);
       }
index 2427167..a6ae630 100644 (file)
@@ -624,13 +624,13 @@ public class StockholmFileTest
   {
     for (char ch : new char[] { '{', '}', '[', ']', '(', ')', '<', '>' })
     {
-      Assert.assertTrue(StockholmFile.DETECT_BRACKETS.matchAt("" + ch, 0),
-              "Didn't recognise " + ch + " as a WUSS bracket");
+      Assert.assertTrue(StockholmFile.RNASS_BRACKETS.indexOf(ch) >= 0,
+              "Didn't recognise '" + ch + "' as a WUSS bracket");
     }
-    for (char ch : new char[] { '@', '!', 'V', 'Q', '*', ' ', '-', '.' })
+    for (char ch : new char[] { '@', '!', '*', ' ', '-', '.' })
     {
-      Assert.assertFalse(StockholmFile.DETECT_BRACKETS.matchAt("" + ch, 0),
-              "Shouldn't recognise " + ch + " as a WUSS bracket");
+      Assert.assertFalse(StockholmFile.RNASS_BRACKETS.indexOf(ch) >= 0,
+              "Shouldn't recognise '" + ch + "' as a WUSS bracket");
     }
   }
   private static void roundTripSSForRNA(String aliFile, String annFile)
@@ -655,4 +655,79 @@ public class StockholmFileTest
     testAlignmentEquivalence(al, newAl, true, true, true);
 
   }
+
+  // this is the single sequence alignment and the SS annotations equivalent to
+  // the ones in file RnaSSTestFile
+  String aliFileRnaSSAlphaChars = ">Test.sequence/1-14\n"
+          + "GUACAAAAAAAAAA";
+  String annFileRnaSSAlphaChars = "JALVIEW_ANNOTATION\n"
+          + "# Created: Thu Aug 02 14:54:57 BST 2018\n" + "\n"
+          + "NO_GRAPH\tSecondary Structure\tSecondary Structure\t<,<|(,(|E,E|H,H|B,B|h,h|e,e|b,b|(,(|E,E|),)|e,e|),)|>,>|\t2.0\n"
+          + "\n"
+          + "ROWPROPERTIES\tSecondary Structure\tscaletofit=true\tshowalllabs=true\tcentrelabs=false\n"
+          + "\n" + "\n" + "ALIGNMENT\tID=RNA.SS.TEST\tTP=RNA;";
+  String wrongAnnFileRnaSSAlphaChars = "JALVIEW_ANNOTATION\n"
+          + "# Created: Thu Aug 02 14:54:57 BST 2018\n" + "\n"
+          + "NO_GRAPH\tSecondary Structure\tSecondary Structure\t<,<|(,(|H,H|E,E|B,B|h,h|e,e|b,b|(,(|E,E|),)|e,e|),)|>,>|\t2.0\n"
+          + "\n"
+          + "ROWPROPERTIES\tSecondary Structure\tscaletofit=true\tshowalllabs=true\tcentrelabs=false\n"
+          + "\n" + "\n" + "ALIGNMENT\tID=RNA.SS.TEST\tTP=RNA;";
+  @Test(groups = { "Functional" })
+  public void stockholmFileRnaSSAlphaChars() throws Exception
+  {
+    AppletFormatAdapter af = new AppletFormatAdapter();
+    AlignmentI al = af.readFile(RnaSSTestFile, DataSourceType.FILE,
+            jalview.io.FileFormat.Stockholm);
+    Iterable<AlignmentAnnotation> aai = al.findAnnotations(null, null,
+            "Secondary Structure");
+    AlignmentAnnotation aa = aai.iterator().next();
+    Assert.assertTrue(aa.isRNA(),
+            "'" + RnaSSTestFile + "' not recognised as RNA SS");
+    Assert.assertTrue(aa.isValidStruc(),
+            "'" + RnaSSTestFile + "' not recognised as valid structure");
+    Annotation[] as = aa.annotations;
+    char[] As = new char[as.length];
+    for (int i = 0; i < as.length; i++)
+    {
+      As[i] = as[i].secondaryStructure;
+    }
+    char[] shouldBe = { '<', '(', 'E', 'H', 'B', 'h', 'e', 'b', '(', 'E',
+        ')', 'e', ')', '>' };
+    Assert.assertTrue(
+            Arrays.equals(As, shouldBe),
+            "Annotation is " + new String(As) + " but should be "
+                    + new String(shouldBe));
+
+    // this should result in the same RNA SS Annotations
+    AlignmentI newAl = new AppletFormatAdapter().readFile(
+            aliFileRnaSSAlphaChars,
+            DataSourceType.PASTE, jalview.io.FileFormat.Fasta);
+    AnnotationFile aaf = new AnnotationFile();
+    aaf.readAnnotationFile(newAl, annFileRnaSSAlphaChars,
+            DataSourceType.PASTE);
+
+    Assert.assertTrue(
+            testRnaSSAnnotationsEquivalent(al.getAlignmentAnnotation()[0],
+                    newAl.getAlignmentAnnotation()[0]));
+
+    // this should NOT result in the same RNA SS Annotations
+    newAl = new AppletFormatAdapter().readFile(
+            aliFileRnaSSAlphaChars, DataSourceType.PASTE,
+            jalview.io.FileFormat.Fasta);
+    aaf = new AnnotationFile();
+    aaf.readAnnotationFile(newAl, wrongAnnFileRnaSSAlphaChars,
+            DataSourceType.PASTE);
+
+    boolean mismatch = testRnaSSAnnotationsEquivalent(al.getAlignmentAnnotation()[0],
+            newAl.getAlignmentAnnotation()[0]);
+    Assert.assertFalse( mismatch );
+  }
+
+  private static boolean testRnaSSAnnotationsEquivalent(
+          AlignmentAnnotation a1,
+          AlignmentAnnotation a2)
+  {
+    return a1.rnaSecondaryStructureEquivalent(a2);
+  }
+
 }