From bab4b08733a9543bdce5c0b9d1c32ca7d18ad9e3 Mon Sep 17 00:00:00 2001 From: Ben Soares Date: Thu, 2 Aug 2018 17:30:15 +0100 Subject: [PATCH] JAL-3053 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 | 4 +- src/jalview/datamodel/AlignmentAnnotation.java | 66 +++++++++++++++++- src/jalview/datamodel/SequenceFeature.java | 27 ++++++-- src/jalview/io/StockholmFile.java | 39 ++++------- test/jalview/io/StockholmFileTest.java | 85 ++++++++++++++++++++++-- 5 files changed, 183 insertions(+), 38 deletions(-) diff --git a/examples/rna_ss_test.stk b/examples/rna_ss_test.stk index e716e0c..429612e 100644 --- a/examples/rna_ss_test.stk +++ b/examples/rna_ss_test.stk @@ -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)> // diff --git a/src/jalview/datamodel/AlignmentAnnotation.java b/src/jalview/datamodel/AlignmentAnnotation.java index f84b18d..ee9389c 100755 --- a/src/jalview/datamodel/AlignmentAnnotation.java +++ b/src/jalview/datamodel/AlignmentAnnotation.java @@ -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 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; } + } diff --git a/src/jalview/datamodel/SequenceFeature.java b/src/jalview/datamodel/SequenceFeature.java index 9c4087e..f17bd33 100755 --- a/src/jalview/datamodel/SequenceFeature.java +++ b/src/jalview/datamodel/SequenceFeature.java @@ -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(); + otherDetails = new HashMap<>(); for (Entry 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(); + 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(); + links = new Vector<>(); } if (!links.contains(labelLink)) @@ -394,7 +395,7 @@ public class SequenceFeature implements FeatureLocationI { if (otherDetails == null) { - otherDetails = new HashMap(); + 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 +{ + @Override + public int compare(SequenceFeature a, SequenceFeature b) + { + return a.getEnd() - b.getEnd(); + } +} + +class SFSortByBegin implements Comparator +{ + @Override + public int compare(SequenceFeature a, SequenceFeature b) + { + return a.getBegin() - b.getBegin(); + } +} diff --git a/src/jalview/io/StockholmFile.java b/src/jalview/io/StockholmFile.java index 71e369e..606540f 100644 --- a/src/jalview/io/StockholmFile.java +++ b/src/jalview/io/StockholmFile.java @@ -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 seqs = new LinkedHashMap<>(); + LinkedHashMap seqs = new LinkedHashMap(); 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 newStruc = new Vector<>(); + Vector newStruc = new Vector(); 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 dbrs = new ArrayList<>(); + List dbrs = new ArrayList(); 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); } diff --git a/test/jalview/io/StockholmFileTest.java b/test/jalview/io/StockholmFileTest.java index 2427167..a6ae630 100644 --- a/test/jalview/io/StockholmFileTest.java +++ b/test/jalview/io/StockholmFileTest.java @@ -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 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); + } + } -- 1.7.10.2