a3328462eebbb44a1122af17ae5088ae8695235e
[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 implements
34         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    * Constructor
47    * 
48    * @param source
49    * @throws IOException
50    */
51   public ScoreMatrixFile(FileParse source) throws IOException
52   {
53     super(false, source);
54   }
55
56   @Override
57   public String print(SequenceI[] sqs, boolean jvsuffix)
58   {
59     return null;
60   }
61
62   /**
63    * Parses the score matrix file, and if successful registers the matrix so it
64    * will be shown in Jalview menus.
65    */
66   @Override
67   public void parse() throws IOException
68   {
69     ScoreMatrix sm = parseMatrix();
70
71     ScoreModels.getInstance().registerScoreModel(sm);
72   }
73
74   /**
75    * Parses the score matrix file and constructs a ScoreMatrix object. If an
76    * error is found in parsing, it is thrown as FileFormatException. Any
77    * warnings are written to syserr.
78    * 
79    * @return
80    * @throws IOException
81    */
82   public ScoreMatrix parseMatrix() throws IOException
83   {
84     ScoreMatrix sm = null;
85     int lineNo = 0;
86     String name = null;
87     String alphabet = null;
88     float[][] scores = null;
89     int size = 0;
90     int row = 0;
91     String err = null;
92     String data;
93
94     while ((data = nextLine()) != null)
95     {
96       lineNo++;
97       data = data.trim();
98       if (data.startsWith(COMMENT_CHAR) || data.length() == 0)
99       {
100         continue;
101       }
102       if (data.toUpperCase().startsWith(SCOREMATRIX))
103       {
104         /*
105          * Parse name from ScoreMatrix <name>
106          * we allow any delimiter after ScoreMatrix then take the rest of the line
107          */
108         if (name != null)
109         {
110           System.err
111                   .println("Warning: 'ScoreMatrix' repeated in file at line "
112                           + lineNo);
113         }
114         StringTokenizer nameLine = new StringTokenizer(data, DELIMITERS);
115         if (nameLine.countTokens() < 2)
116         {
117           err = "Format error: expected 'ScoreMatrix <name>', found '"
118                   + data + "' at line " + lineNo;
119           throw new FileFormatException(err);
120         }
121         nameLine.nextToken(); // 'ScoreMatrix'
122         name = nameLine.nextToken(); // next field
123         name = data.substring(1).substring(data.substring(1).indexOf(name));
124         continue;
125       }
126       else if (name == null)
127       {
128         err = "Format error: 'ScoreMatrix <name>' should be the first non-comment line";
129         throw new FileFormatException(err);
130       }
131
132       /*
133        * next line after ScoreMatrix should be the alphabet of scored symbols
134        */
135       if (alphabet == null)
136       {
137         alphabet = data;
138         size = alphabet.length();
139         scores = new float[size][];
140         continue;
141       }
142
143       /*
144        * too much information
145        */
146       if (row >= size)
147       {
148         err = "Unexpected extra input line in score model file: '" + data
149                 + "'";
150         throw new FileFormatException(err);
151       }
152
153       /*
154        * permit an uncommented line with delimited residue headings
155        */
156       if (isHeaderLine(data, alphabet))
157       {
158         continue;
159       }
160
161       /*
162        * subsequent lines should be the symbol scores
163        * optionally with the symbol as the first column for readability
164        */
165       StringTokenizer scoreLine = new StringTokenizer(data, DELIMITERS);
166       int tokenCount = scoreLine.countTokens();
167       if (tokenCount == size + 1)
168       {
169         /*
170          * check 'guide' symbol is the row'th letter of the alphabet
171          */
172         String symbol = scoreLine.nextToken();
173         if (symbol.length() > 1 || symbol.charAt(0) != alphabet.charAt(row))
174         {
175           err = String
176                   .format("Error parsing score matrix at line %d, expected '%s' but found '%s'",
177                           lineNo, alphabet.charAt(row), symbol);
178           throw new FileFormatException(err);
179         }
180       }
181       if (scoreLine.countTokens() != size)
182       {
183         err = String.format("Expected %d scores at line %d but found %d",
184                 size, lineNo, scoreLine.countTokens());
185         throw new FileFormatException(err);
186       }
187       scores[row] = new float[size];
188       int col = 0;
189       String value = null;
190       while (scoreLine.hasMoreTokens())
191       {
192         try
193         {
194           value = scoreLine.nextToken();
195           scores[row][col] = Float.valueOf(value);
196           col++;
197         } catch (NumberFormatException e)
198         {
199           err = String.format(
200                   "Invalid score value '%s' at line %d column %d", value,
201                   lineNo, col);
202           throw new FileFormatException(err);
203         }
204       }
205       row++;
206     }
207
208     /*
209      * out of data - check we found enough
210      */
211     if (row < size)
212     {
213       err = String
214               .format("Expected %d rows of score data in score matrix but only found %d",
215                       size, row);
216       throw new FileFormatException(err);
217     }
218
219     /*
220      * If we get here, then name, alphabet and scores have been parsed successfully
221      */
222     sm = new ScoreMatrix(name, alphabet.toCharArray(), scores);
223     matrixName = name;
224
225     return sm;
226   }
227
228   /**
229    * Answers true if the data line consists of the alphabet characters,
230    * delimited (as to provide a heading row). Otherwise returns false (e.g. if
231    * the data is a row of score values).
232    * 
233    * @param data
234    * @param alphabet
235    * @return
236    */
237   private boolean isHeaderLine(String data, String alphabet)
238   {
239     StringTokenizer scoreLine = new StringTokenizer(data, DELIMITERS);
240     int i = 0;
241     while (scoreLine.hasMoreElements())
242     {
243       /*
244        * skip over characters in the alphabet that are 
245        * also a delimiter (e.g. space)
246        */
247       char symbol = alphabet.charAt(i++);
248       if (!DELIMITERS.contains(String.valueOf(symbol)))
249       {
250         if (!String.valueOf(symbol).equals(scoreLine.nextToken()))
251         {
252           return false;
253         }
254       }
255     }
256     return true;
257   }
258
259   public String getMatrixName()
260   {
261     return matrixName;
262   }
263 }