JAL-2416 support roundtrip print/parse of ScoreMatrix
[jalview.git] / src / jalview / io / ScoreMatrixFile.java
1 package jalview.io;
2
3 import jalview.analysis.scoremodels.ScoreMatrix;
4 import jalview.analysis.scoremodels.ScoreModels;
5 import jalview.datamodel.SequenceI;
6
7 import java.io.IOException;
8 import java.util.StringTokenizer;
9
10 /**
11  * A class that can parse a file containing a substitution matrix and register
12  * it for use in Jalview
13  * 
14  * @author gmcarstairs
15  *
16  */
17 // TODO modify the AlignFile / IdentifyFile pattern so that non-alignment files
18 // like this are handled more naturally
19 public class ScoreMatrixFile extends AlignFile implements
20         AlignmentFileReaderI
21 {
22   // first non-comment line identifier - also checked in IdentifyFile
23   public static final String SCOREMATRIX = "SCOREMATRIX";
24
25   private static final String DELIMITERS = " ,\t";
26
27   private static final String COMMENT_CHAR = "#";
28
29   private String matrixName;
30
31   /**
32    * Constructor
33    * 
34    * @param source
35    * @throws IOException
36    */
37   public ScoreMatrixFile(FileParse source) throws IOException
38   {
39     super(false, source);
40   }
41
42   @Override
43   public String print(SequenceI[] sqs, boolean jvsuffix)
44   {
45     return null;
46   }
47
48   /**
49    * Parses the score matrix file, and if successful registers the matrix so it
50    * will be shown in Jalview menus.
51    */
52   @Override
53   public void parse() throws IOException
54   {
55     ScoreMatrix sm = parseMatrix();
56
57     ScoreModels.getInstance().registerScoreModel(sm);
58   }
59
60   /**
61    * Parses the score matrix file and constructs a ScoreMatrix object. If an
62    * error is found in parsing, it is thrown as FileFormatException. Any
63    * warnings are written to syserr.
64    * 
65    * @return
66    * @throws IOException
67    */
68   public ScoreMatrix parseMatrix() throws IOException
69   {
70     ScoreMatrix sm = null;
71     int lineNo = 0;
72     String name = null;
73     String alphabet = null;
74     float[][] scores = null;
75     int size = 0;
76     int row = 0;
77     String err = null;
78     String data;
79
80     while ((data = nextLine()) != null)
81     {
82       lineNo++;
83       data = data.trim();
84       if (data.startsWith(COMMENT_CHAR) || data.length() == 0)
85       {
86         continue;
87       }
88       if (data.toUpperCase().startsWith(SCOREMATRIX))
89       {
90         /*
91          * Parse name from ScoreMatrix <name>
92          * we allow any delimiter after ScoreMatrix then take the rest of the line
93          */
94         if (name != null)
95         {
96           System.err
97                   .println("Warning: 'ScoreMatrix' repeated in file at line "
98                           + lineNo);
99         }
100         StringTokenizer nameLine = new StringTokenizer(data, DELIMITERS);
101         if (nameLine.countTokens() < 2)
102         {
103           err = "Format error: expected 'ScoreMatrix <name>', found '"
104                   + data + "' at line " + lineNo;
105           throw new FileFormatException(err);
106         }
107         nameLine.nextToken(); // 'ScoreMatrix'
108         name = nameLine.nextToken(); // next field
109         name = data.substring(1).substring(data.substring(1).indexOf(name));
110         continue;
111       }
112       else if (name == null)
113       {
114         err = "Format error: 'ScoreMatrix <name>' should be the first non-comment line";
115         throw new FileFormatException(err);
116       }
117
118       /*
119        * next line after ScoreMatrix should be the alphabet of scored symbols
120        */
121       if (alphabet == null)
122       {
123         alphabet = data;
124         size = alphabet.length();
125         scores = new float[size][];
126         continue;
127       }
128
129       /*
130        * too much information
131        */
132       if (row >= size)
133       {
134         err = "Unexpected extra input line in score model file: '" + data
135                 + "'";
136         throw new FileFormatException(err);
137       }
138
139       /*
140        * permit an uncommented line with delimited residue headings
141        */
142       if (isHeaderLine(data, alphabet))
143       {
144         continue;
145       }
146
147       /*
148        * subsequent lines should be the symbol scores
149        * optionally with the symbol as the first column for readability
150        */
151       StringTokenizer scoreLine = new StringTokenizer(data, DELIMITERS);
152       if (scoreLine.countTokens() == size + 1)
153       {
154         /*
155          * check 'guide' symbol is the row'th letter of the alphabet
156          */
157         String symbol = scoreLine.nextToken();
158         if (symbol.length() > 1 || symbol.charAt(0) != alphabet.charAt(row))
159         {
160           err = String
161                   .format("Error parsing score matrix at line %d, expected '%s' but found '%s'",
162                           lineNo, alphabet.charAt(row), symbol);
163           throw new FileFormatException(err);
164         }
165       }
166       if (scoreLine.countTokens() != size)
167       {
168         err = String.format("Expected %d scores at line %d but found %d",
169                 size, lineNo, scoreLine.countTokens());
170         throw new FileFormatException(err);
171       }
172       scores[row] = new float[size];
173       int col = 0;
174       String value = null;
175       while (scoreLine.hasMoreTokens())
176       {
177         try
178         {
179           value = scoreLine.nextToken();
180           scores[row][col] = Float.valueOf(value);
181           col++;
182         } catch (NumberFormatException e)
183         {
184           err = String.format(
185                   "Invalid score value '%s' at line %d column %d", value,
186                   lineNo, col);
187           throw new FileFormatException(err);
188         }
189       }
190       row++;
191     }
192
193     /*
194      * out of data - check we found enough
195      */
196     if (row < size)
197     {
198       err = String
199               .format("Expected %d rows of score data in score matrix but only found %d",
200                       size, row);
201       throw new FileFormatException(err);
202     }
203
204     /*
205      * If we get here, then name, alphabet and scores have been parsed successfully
206      */
207     sm = new ScoreMatrix(name, alphabet.toCharArray(), scores);
208     matrixName = name;
209
210     return sm;
211   }
212
213   /**
214    * Answers true if the data line consists of the alphabet characters,
215    * delimited (as to provide a heading row). Otherwise returns false (e.g. if
216    * the data is a row of score values).
217    * 
218    * @param data
219    * @param alphabet
220    * @return
221    */
222   private boolean isHeaderLine(String data, String alphabet)
223   {
224     StringTokenizer scoreLine = new StringTokenizer(data, DELIMITERS);
225     int i = 0;
226     while (scoreLine.hasMoreElements())
227     {
228       /*
229        * skip over characters in the alphabet that are 
230        * also a delimiter (e.g. space)
231        */
232       char symbol = alphabet.charAt(i++);
233       if (!DELIMITERS.contains(String.valueOf(symbol)))
234       {
235         if (!String.valueOf(symbol).equals(scoreLine.nextToken()))
236         {
237           return false;
238         }
239       }
240     }
241     return true;
242   }
243
244   public String getMatrixName()
245   {
246     return matrixName;
247   }
248 }