ad1527cbb22c9c4585728ede836121deb1f3d1ea
[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  * <p>
14  * Accepts 'NCBI' format (e.g.
15  * https://www.ncbi.nlm.nih.gov/Class/FieldGuide/BLOSUM62.txt), with the
16  * addition of a header line to provide a matrix name, e.g.
17  * 
18  * <pre>
19  * ScoreMatrix BLOSUM62
20  * </pre>
21  * 
22  * Also accepts 'AAindex' format (as described at
23  * http://www.genome.jp/aaindex/aaindex_help.html) with the minimum data
24  * required being
25  * 
26  * <pre>
27  * H accession number (used as score matrix identifier in Jalview)
28  * D description (used for tooltip in Jalview)
29  * M rows = symbolList
30  * and the substitution scores
31  * </pre>
32  */
33 public class ScoreMatrixFile extends AlignFile
34         implements AlignmentFileReaderI
35 {
36   // first non-comment line identifier - also checked in IdentifyFile
37   public static final String SCOREMATRIX = "SCOREMATRIX";
38
39   private static final String DELIMITERS = " ,\t";
40
41   private static final String COMMENT_CHAR = "#";
42
43   private String matrixName;
44
45   /*
46    * aaindex format has scores for diagonal and below only
47    */
48   boolean isLowerDiagonalOnly;
49
50   /*
51    * ncbi format has symbols as first column on score rows
52    */
53   boolean hasGuideColumn;
54
55   /**
56    * Constructor
57    * 
58    * @param source
59    * @throws IOException
60    */
61   public ScoreMatrixFile(FileParse source) throws IOException
62   {
63     super(false, source);
64   }
65
66   @Override
67   public String print(SequenceI[] sqs, boolean jvsuffix)
68   {
69     return null;
70   }
71
72   /**
73    * Parses the score matrix file, and if successful registers the matrix so it
74    * will be shown in Jalview menus. This method is not thread-safe (a separate
75    * instance of this class should be used by each thread).
76    */
77   @Override
78   public void parse() throws IOException
79   {
80     ScoreMatrix sm = parseMatrix();
81
82     ScoreModels.getInstance().registerScoreModel(sm);
83   }
84
85   /**
86    * Parses the score matrix file and constructs a ScoreMatrix object. If an
87    * error is found in parsing, it is thrown as FileFormatException. Any
88    * warnings are written to syserr.
89    * 
90    * @return
91    * @throws IOException
92    */
93   public ScoreMatrix parseMatrix() throws IOException
94   {
95     ScoreMatrix sm = null;
96     int lineNo = 0;
97     String name = null;
98     char[] alphabet = null;
99     float[][] scores = null;
100     int size = 0;
101     int row = 0;
102     String err = null;
103     String data;
104     isLowerDiagonalOnly = false;
105
106     while ((data = nextLine()) != null)
107     {
108       lineNo++;
109       data = data.trim();
110       if (data.startsWith(COMMENT_CHAR) || data.length() == 0)
111       {
112         continue;
113       }
114       if (data.toUpperCase().startsWith(SCOREMATRIX))
115       {
116         /*
117          * Parse name from ScoreMatrix <name>
118          * we allow any delimiter after ScoreMatrix then take the rest of the line
119          */
120         if (name != null)
121         {
122           throw new FileFormatException(
123                   "Error: 'ScoreMatrix' repeated in file at line "
124                           + lineNo);
125         }
126         StringTokenizer nameLine = new StringTokenizer(data, DELIMITERS);
127         if (nameLine.countTokens() < 2)
128         {
129           err = "Format error: expected 'ScoreMatrix <name>', found '"
130                   + data + "' at line " + lineNo;
131           throw new FileFormatException(err);
132         }
133         nameLine.nextToken(); // 'ScoreMatrix'
134         name = nameLine.nextToken(); // next field
135         name = data.substring(1).substring(data.substring(1).indexOf(name));
136         continue;
137       }
138       else if (data.startsWith("H ") && name == null)
139       {
140         /*
141          * AAindex identifier 
142          */
143         return parseAAIndexFormat(lineNo, data);
144       }
145       else if (name == null)
146       {
147         err = "Format error: 'ScoreMatrix <name>' should be the first non-comment line";
148         throw new FileFormatException(err);
149       }
150
151       /*
152        * next non-comment line after ScoreMatrix should be the 
153        * column header line with the alphabet of scored symbols
154        */
155       if (alphabet == null)
156       {
157         StringTokenizer columnHeadings = new StringTokenizer(data,
158                 DELIMITERS);
159         size = columnHeadings.countTokens();
160         alphabet = new char[size];
161         int col = 0;
162         while (columnHeadings.hasMoreTokens())
163         {
164           alphabet[col++] = columnHeadings.nextToken().charAt(0);
165         }
166         scores = new float[size][];
167         continue;
168       }
169
170       /*
171        * too much information
172        */
173       if (row >= size)
174       {
175         err = "Unexpected extra input line in score model file: '" + data
176                 + "'";
177         throw new FileFormatException(err);
178       }
179
180       parseValues(data, lineNo, scores, row, alphabet);
181       row++;
182     }
183
184     /*
185      * out of data - check we found enough
186      */
187     if (row < size)
188     {
189       err = String.format(
190               "Expected %d rows of score data in score matrix but only found %d",
191               size, row);
192       throw new FileFormatException(err);
193     }
194
195     /*
196      * If we get here, then name, alphabet and scores have been parsed successfully
197      */
198     sm = new ScoreMatrix(name, alphabet, scores);
199     matrixName = name;
200
201     return sm;
202   }
203
204   /**
205    * Parse input as AAIndex format, starting from the header line with the
206    * accession id
207    * 
208    * @param lineNo
209    * @param data
210    * @return
211    * @throws IOException
212    */
213   protected ScoreMatrix parseAAIndexFormat(int lineNo, String data)
214           throws IOException
215   {
216     String name = data.substring(2).trim();
217     String description = null;
218
219     float[][] scores = null;
220     char[] alphabet = null;
221     int row = 0;
222     int size = 0;
223
224     while ((data = nextLine()) != null)
225     {
226       lineNo++;
227       data = data.trim();
228       if (skipAAindexLine(data))
229       {
230         continue;
231       }
232       if (data.startsWith("D "))
233       {
234         description = data.substring(2).trim();
235       }
236       else if (data.startsWith("M "))
237       {
238         alphabet = parseAAindexRowsColumns(lineNo, data);
239         size = alphabet.length;
240         scores = new float[size][size];
241       }
242       else if (scores == null)
243       {
244         throw new FileFormatException(
245                 "No alphabet specified in matrix file");
246       }
247       else if (row >= size)
248       {
249         throw new FileFormatException("Too many data rows in matrix file");
250       }
251       else
252       {
253         parseValues(data, lineNo, scores, row, alphabet);
254         row++;
255       }
256     }
257
258     ScoreMatrix sm = new ScoreMatrix(name, description, alphabet, scores);
259     matrixName = name;
260
261     return sm;
262   }
263
264   /**
265    * Parse one row of score values, delimited by whitespace or commas. The line
266    * may optionally include the symbol from which the scores are defined. Values
267    * may be present for all columns, or only up to the diagonal (in which case
268    * upper diagonal values are set symmetrically).
269    * 
270    * @param data
271    *          the line to be parsed
272    * @param lineNo
273    * @param scores
274    *          the score matrix to add data to
275    * @param row
276    *          the row number / alphabet index position
277    * @param alphabet
278    * @return
279    * @throws exception
280    *           if invalid, or too few, or too many values
281    */
282   protected void parseValues(String data, int lineNo, float[][] scores,
283           int row, char[] alphabet) throws FileFormatException
284   {
285     String err;
286     int size = alphabet.length;
287     StringTokenizer scoreLine = new StringTokenizer(data, DELIMITERS);
288
289     int tokenCount = scoreLine.countTokens();
290
291     /*
292      * inspect first row to see if it includes the symbol in the first column,
293      * and to see if it is lower diagonal values only (i.e. just one score)
294      */
295     if (row == 0)
296     {
297       if (data.startsWith(String.valueOf(alphabet[0])))
298       {
299         hasGuideColumn = true;
300       }
301       if (tokenCount == (hasGuideColumn ? 2 : 1))
302       {
303         isLowerDiagonalOnly = true;
304       }
305     }
306
307     if (hasGuideColumn)
308     {
309       /*
310        * check 'guide' symbol is the row'th letter of the alphabet
311        */
312       String symbol = scoreLine.nextToken();
313       if (symbol.length() > 1 || symbol.charAt(0) != alphabet[row])
314       {
315         err = String.format(
316                 "Error parsing score matrix at line %d, expected '%s' but found '%s'",
317                 lineNo, alphabet[row], symbol);
318         throw new FileFormatException(err);
319       }
320       tokenCount = scoreLine.countTokens(); // excluding guide symbol
321     }
322
323     /*
324      * check the right number of values (lower diagonal or full format)
325      */
326     if (isLowerDiagonalOnly && tokenCount != row + 1)
327     {
328       err = String.format(
329               "Expected %d scores at line %d: '%s' but found %d", row + 1,
330               lineNo, data, tokenCount);
331       throw new FileFormatException(err);
332     }
333
334     if (!isLowerDiagonalOnly && tokenCount != size)
335     {
336       err = String.format(
337               "Expected %d scores at line %d: '%s' but found %d", size,
338               lineNo, data, scoreLine.countTokens());
339       throw new FileFormatException(err);
340     }
341
342     /*
343      * parse and set the values, setting the symmetrical value
344      * as well if lower diagonal format data
345      */
346     scores[row] = new float[size];
347     int col = 0;
348     String value = null;
349     while (scoreLine.hasMoreTokens())
350     {
351       try
352       {
353         value = scoreLine.nextToken();
354         scores[row][col] = Float.valueOf(value);
355         if (isLowerDiagonalOnly)
356         {
357           scores[col][row] = scores[row][col];
358         }
359         col++;
360       } catch (NumberFormatException e)
361       {
362         err = String.format("Invalid score value '%s' at line %d column %d",
363                 value, lineNo, col);
364         throw new FileFormatException(err);
365       }
366     }
367   }
368
369   /**
370    * Parse the line in an aaindex file that looks like
371    * 
372    * <pre>
373    * M rows = ARNDCQEGHILKMFPSTWYV, cols = ARNDCQEGHILKMFPSTWYV
374    * </pre>
375    * 
376    * rejecting it if rows and cols do not match. Returns the string of
377    * characters in the row/cols alphabet.
378    * 
379    * @param lineNo
380    * @param data
381    * @return
382    * @throws FileFormatException
383    */
384   protected char[] parseAAindexRowsColumns(int lineNo, String data)
385           throws FileFormatException
386   {
387     String err = "Unexpected aaIndex score matrix data at line " + lineNo
388             + ": " + data;
389
390     try
391     {
392       String[] toks = data.split(",");
393       String rowsAlphabet = toks[0].split("=")[1].trim();
394       String colsAlphabet = toks[1].split("=")[1].trim();
395       if (!rowsAlphabet.equals(colsAlphabet))
396       {
397         throw new FileFormatException("rows != cols");
398       }
399       return rowsAlphabet.toCharArray();
400     } catch (Throwable t)
401     {
402       throw new FileFormatException(err + " " + t.getMessage());
403     }
404   }
405
406   /**
407    * Answers true if line is one we are not interested in from AAindex format
408    * file
409    * 
410    * @param data
411    * @return
412    */
413   protected boolean skipAAindexLine(String data)
414   {
415     if (data.startsWith(COMMENT_CHAR) || data.length() == 0)
416     {
417       return true;
418     }
419     if (data.startsWith("*") || data.startsWith("R ")
420             || data.startsWith("A ") || data.startsWith("T ")
421             || data.startsWith("J ") || data.startsWith("//"))
422     {
423       return true;
424     }
425     return false;
426   }
427
428   public String getMatrixName()
429   {
430     return matrixName;
431   }
432 }