1 | package nl.tno.massSequencing |
---|
2 | |
---|
3 | import org.codehaus.groovy.grails.commons.ApplicationHolder as AH |
---|
4 | |
---|
5 | /** |
---|
6 | * Represents a samples that is used in an assay. |
---|
7 | * |
---|
8 | * @author Robert Horlings (robert@isdat.nl) |
---|
9 | * |
---|
10 | */ |
---|
11 | class AssaySample { |
---|
12 | // Grails datasource is used for executing custom SQL statement |
---|
13 | def dataSource |
---|
14 | |
---|
15 | // To be computed at run time (and saved in cache) |
---|
16 | private float _averageQuality = -1.0; |
---|
17 | private long _numQualScores = -1; |
---|
18 | private long _numSequenceFiles = -1; |
---|
19 | private long _numQualityFiles = -1; |
---|
20 | |
---|
21 | Integer numUniqueSequences // Number of unique sequences / OTUs. Is only available after preprocessing |
---|
22 | Long numSequences // Number of sequences in this assaySample. Is used in many calculations and therefore stored |
---|
23 | |
---|
24 | String fwOligo |
---|
25 | String fwMidName |
---|
26 | String fwTotalSeq |
---|
27 | String fwMidSeq |
---|
28 | String fwPrimerSeq |
---|
29 | |
---|
30 | String revOligo |
---|
31 | String revMidName |
---|
32 | String revTotalSeq |
---|
33 | String revMidSeq |
---|
34 | String revPrimerSeq |
---|
35 | |
---|
36 | static belongsTo = [ assay: Assay, sample: Sample, run: Run ] |
---|
37 | static hasMany = [ sequenceData: SequenceData ] |
---|
38 | |
---|
39 | static constraints = { |
---|
40 | numUniqueSequences(nullable: true) |
---|
41 | numSequences(nullable: true) |
---|
42 | fwOligo(nullable: true) |
---|
43 | fwMidName(nullable: true) |
---|
44 | fwTotalSeq(nullable:true) |
---|
45 | fwMidSeq(nullable:true) |
---|
46 | fwPrimerSeq(nullable:true) |
---|
47 | revOligo(nullable: true) |
---|
48 | revMidName(nullable: true) |
---|
49 | revTotalSeq(nullable:true) |
---|
50 | revMidSeq(nullable:true) |
---|
51 | revPrimerSeq(nullable:true) |
---|
52 | run(nullable: true); |
---|
53 | } |
---|
54 | |
---|
55 | static mapping = { |
---|
56 | |
---|
57 | sequenceData cascade: "all-delete-orphan" |
---|
58 | sample fetch: 'join' |
---|
59 | assay fetch: 'join' |
---|
60 | run fetch: 'join' |
---|
61 | } |
---|
62 | |
---|
63 | /** |
---|
64 | * Returns the number of files in the system, belonging to this |
---|
65 | * assay-sample combination. |
---|
66 | * |
---|
67 | * @return |
---|
68 | */ |
---|
69 | public int numFiles() { |
---|
70 | return numSequenceFiles() + numQualityFiles(); |
---|
71 | } |
---|
72 | |
---|
73 | /** |
---|
74 | * Returns the number of sequences that have been classified for this sample |
---|
75 | * @return |
---|
76 | */ |
---|
77 | public int numClassifiedSequences() { |
---|
78 | return Sequence.countByAssaySample( this ); |
---|
79 | } |
---|
80 | |
---|
81 | /** |
---|
82 | * Returns the number of sequence files in the system, belonging to this |
---|
83 | * assay-sample combination. |
---|
84 | * |
---|
85 | * @return |
---|
86 | */ |
---|
87 | public int numSequenceFiles() { |
---|
88 | if( _numSequenceFiles > -1 ) |
---|
89 | return _numSequenceFiles; |
---|
90 | |
---|
91 | if( !sequenceData ) |
---|
92 | return 0 |
---|
93 | |
---|
94 | int numFiles = 0; |
---|
95 | sequenceData.each { |
---|
96 | if( it.sequenceFile ) |
---|
97 | numFiles++ |
---|
98 | } |
---|
99 | |
---|
100 | return numFiles; |
---|
101 | } |
---|
102 | |
---|
103 | /** |
---|
104 | * Returns the number of quality files in the system, belonging to this |
---|
105 | * assay-sample combination. |
---|
106 | * |
---|
107 | * @return |
---|
108 | */ |
---|
109 | public int numQualityFiles() { |
---|
110 | if( _numQualityFiles > -1 ) |
---|
111 | return _numQualityFiles; |
---|
112 | |
---|
113 | if( !sequenceData ) |
---|
114 | return 0 |
---|
115 | |
---|
116 | int numFiles = 0; |
---|
117 | sequenceData.each { |
---|
118 | if( it.qualityFile ) |
---|
119 | numFiles++ |
---|
120 | } |
---|
121 | |
---|
122 | return numFiles; |
---|
123 | } |
---|
124 | |
---|
125 | /** |
---|
126 | * Returns the number of sequences in the files on the system, belonging to this |
---|
127 | * assay-sample combination. |
---|
128 | * |
---|
129 | * @return |
---|
130 | */ |
---|
131 | public long numSequences() { |
---|
132 | return numSequences ?: 0; |
---|
133 | } |
---|
134 | |
---|
135 | /** |
---|
136 | * Returns the number of quality scores in the files on the system, belonging to this |
---|
137 | * assay-sample combination. |
---|
138 | * |
---|
139 | * @return |
---|
140 | */ |
---|
141 | public long numQualScores() { |
---|
142 | if( _numQualScores > -1 ) |
---|
143 | return _numQualScores; |
---|
144 | |
---|
145 | if( !sequenceData ) |
---|
146 | return 0 |
---|
147 | |
---|
148 | long numQualScores = 0; |
---|
149 | sequenceData.each { numQualScores += it.numQualScores() } |
---|
150 | |
---|
151 | // Save as cache |
---|
152 | _numQualScores = numQualScores; |
---|
153 | |
---|
154 | return numQualScores; |
---|
155 | } |
---|
156 | /** |
---|
157 | * Returns the average quality of the sequences in the files on the system, |
---|
158 | * belonging to this assay-sample combination. |
---|
159 | * |
---|
160 | * @return |
---|
161 | */ |
---|
162 | public float averageQuality() { |
---|
163 | if( _averageQuality > -1 ) |
---|
164 | return _averageQuality; |
---|
165 | |
---|
166 | if( !sequenceData ) |
---|
167 | return 0.0 |
---|
168 | |
---|
169 | int numSequences = 0; |
---|
170 | float averageQuality = 0.0; |
---|
171 | |
---|
172 | sequenceData.each { |
---|
173 | numSequences += it.numSequences |
---|
174 | averageQuality = averageQuality + ( it.averageQuality - averageQuality ) / numSequences * it.numSequences; |
---|
175 | } |
---|
176 | |
---|
177 | // Save as cache |
---|
178 | _averageQuality = averageQuality; |
---|
179 | |
---|
180 | return averageQuality; |
---|
181 | } |
---|
182 | |
---|
183 | /** |
---|
184 | * Reset the statistics to their default value, in order to ensure that the values are recomputed next time. |
---|
185 | */ |
---|
186 | public void resetStats() { |
---|
187 | _numQualScores = -1; |
---|
188 | _averageQuality = -1; |
---|
189 | |
---|
190 | _numSequenceFiles = -1; |
---|
191 | _numQualityFiles = -1; |
---|
192 | } |
---|
193 | |
---|
194 | /** |
---|
195 | * Fill statistics for multiple assaySamples at once. |
---|
196 | * |
---|
197 | * This method is used to improve performance. If multiple assaysamples are shown on the screen |
---|
198 | * and all have to collect the statistics, it is done with n queries (where n is the number of assaysamples) |
---|
199 | * This method reduces this number to 1 |
---|
200 | * |
---|
201 | * @param assaySamples List of assaySamples to collect the statistics for |
---|
202 | */ |
---|
203 | public static void initStats( ArrayList<AssaySample> assaySamples ) { |
---|
204 | if( !assaySamples ) |
---|
205 | return; |
---|
206 | |
---|
207 | // Order assaysamples by id |
---|
208 | assaySamples.sort { it.id } |
---|
209 | |
---|
210 | // Retrieve the datasource for these assaysamples |
---|
211 | groovy.sql.Sql sql = new groovy.sql.Sql(assaySamples[0].dataSource) |
---|
212 | |
---|
213 | // Execute a custom query |
---|
214 | String ids = assaySamples.id*.toString().join( ', ' ); |
---|
215 | String sqlStatement = """ |
---|
216 | SELECT |
---|
217 | a.id, |
---|
218 | count( s.sequence_file ) AS numSequenceFiles, |
---|
219 | count( s.quality_file ) AS numQualityFiles, |
---|
220 | sum( CASE WHEN s.quality_file IS NOT NULL THEN s.num_sequences ELSE 0 END ) AS numQualScores |
---|
221 | FROM assay_sample a |
---|
222 | LEFT JOIN sequence_data s ON a.id = s.sample_id |
---|
223 | WHERE a.id IN (${ids}) |
---|
224 | GROUP BY a.id |
---|
225 | ORDER BY a.id |
---|
226 | """ |
---|
227 | |
---|
228 | // For each assaysample, one row is returned. In order to prevent lookups in |
---|
229 | // the asasySamples list, both lists are sorted by id. For that reason, we can just |
---|
230 | // walk through both lists simultaneously |
---|
231 | def listIndex = 0; |
---|
232 | sql.eachRow( sqlStatement ) { |
---|
233 | // Still, we perform a check to see whether the ids match. If they don't |
---|
234 | // something went wrong in the database or in grails. We note an error, and |
---|
235 | // skip the assignment. The variables will be filled later on anyhow, it |
---|
236 | // will only be a little bit slower |
---|
237 | if( it.id != assaySamples[ listIndex ]?.id ) { |
---|
238 | log.error "ID of the database row and the domain object don't match. DB: " + it.id + ", Domain object: " + assaySamples[ listIndex ]?.id |
---|
239 | } else { |
---|
240 | assaySamples[ listIndex ]._numQualScores = it.numQualScores ?: 0; |
---|
241 | assaySamples[ listIndex ]._numSequenceFiles = it.numSequenceFiles ?: 0; |
---|
242 | assaySamples[ listIndex ]._numQualityFiles = it.numQualityFiles ?: 0; |
---|
243 | } |
---|
244 | |
---|
245 | listIndex++; |
---|
246 | } |
---|
247 | } |
---|
248 | |
---|
249 | /** |
---|
250 | * Check whether this assay-sample combination contains information that should be saved in trash on a delete |
---|
251 | * @return |
---|
252 | */ |
---|
253 | public boolean containsData() { |
---|
254 | return fwOligo || fwMidName || fwTotalSeq || fwMidSeq || fwPrimerSeq || |
---|
255 | revOligo || revMidName || revTotalSeq || revMidSeq || revPrimerSeq || |
---|
256 | numFiles() > 0 || numClassifiedSequences() > 0; |
---|
257 | } |
---|
258 | |
---|
259 | /** |
---|
260 | * Move information that should be kept on delete to another assaySample object. |
---|
261 | * |
---|
262 | * N.B. The sequencedata objects are really moved, so removed from the original object! |
---|
263 | * |
---|
264 | * @param otherAssaySample Object to move |
---|
265 | */ |
---|
266 | public void moveValuableDataTo( AssaySample otherAssaySample ) { |
---|
267 | // Copy properties |
---|
268 | otherAssaySample.fwOligo = fwOligo; |
---|
269 | otherAssaySample.fwMidName = fwMidName; |
---|
270 | otherAssaySample.fwTotalSeq = fwTotalSeq; |
---|
271 | otherAssaySample.fwMidSeq = fwMidSeq; |
---|
272 | otherAssaySample.fwPrimerSeq = fwPrimerSeq; |
---|
273 | |
---|
274 | otherAssaySample.revOligo = revOligo; |
---|
275 | otherAssaySample.revMidName = revMidName; |
---|
276 | otherAssaySample.revTotalSeq = revTotalSeq; |
---|
277 | otherAssaySample.revMidSeq = revMidSeq; |
---|
278 | otherAssaySample.revPrimerSeq = revPrimerSeq; |
---|
279 | |
---|
280 | otherAssaySample.numSequences = numSequences; |
---|
281 | |
---|
282 | // Move attached data |
---|
283 | def dataList = [] + sequenceData?.toList() |
---|
284 | def otherAssay = otherAssaySample.assay; |
---|
285 | |
---|
286 | if( dataList && dataList.size() > 0 ) { |
---|
287 | for( def j = dataList.size() - 1; j >= 0; j-- ) { |
---|
288 | // Copy data to a new sequencedata object. |
---|
289 | // Just moving the sequencedata object to the other assay sample resulted |
---|
290 | // in a 'deleted object would be re-saved by cascade' exception |
---|
291 | |
---|
292 | if( dataList[ j ] ) { |
---|
293 | // Clone the sequencedata object |
---|
294 | def sd = dataList[ j ]?.clone(); |
---|
295 | |
---|
296 | if( sd ) |
---|
297 | otherAssaySample.addToSequenceData( sd ); |
---|
298 | |
---|
299 | // Copy all sequence classifications to the new sequenceData |
---|
300 | Sequence.executeUpdate( "UPDATE Sequence s SET s.sequenceData = :new WHERE s.sequenceData = :old ", [ 'old': dataList[ j ], 'new': sd ] ) |
---|
301 | |
---|
302 | // Remove the old sequencedata object |
---|
303 | this.removeFromSequenceData( dataList[ j ] ); |
---|
304 | } |
---|
305 | } |
---|
306 | } |
---|
307 | |
---|
308 | // Copy run properties |
---|
309 | if( otherAssaySample.run ) { |
---|
310 | otherAssaySample.run.removeFromAssaySamples( otherAssaySample ); |
---|
311 | } |
---|
312 | |
---|
313 | // Remove this sample from the run. |
---|
314 | if( run ) { |
---|
315 | def copyRun = run; |
---|
316 | copyRun.removeFromAssaySamples( this ); |
---|
317 | copyRun.addToAssaySamples( otherAssaySample ); |
---|
318 | } else { |
---|
319 | otherAssaySample.run = null; |
---|
320 | } |
---|
321 | } |
---|
322 | |
---|
323 | |
---|
324 | /** |
---|
325 | * Delete all sequences from a sample |
---|
326 | * @param assaySample |
---|
327 | * @return |
---|
328 | */ |
---|
329 | public int deleteSequenceData() { |
---|
330 | if( !sequenceData ) |
---|
331 | return 0; |
---|
332 | |
---|
333 | def numFiles = 0; |
---|
334 | def data = [] + sequenceData |
---|
335 | data.each { sequenceData -> |
---|
336 | numFiles += sequenceData.numFiles(); |
---|
337 | |
---|
338 | removeFromSequenceData( sequenceData ); |
---|
339 | sequenceData.delete(flush:true); |
---|
340 | } |
---|
341 | |
---|
342 | numSequences = 0; |
---|
343 | |
---|
344 | resetStats(); |
---|
345 | save(); |
---|
346 | |
---|
347 | return numFiles; |
---|
348 | } |
---|
349 | |
---|
350 | |
---|
351 | /** |
---|
352 | * Remove data from associations before delete the assaySample itself |
---|
353 | */ |
---|
354 | def beforeDelete = { |
---|
355 | deleteSequenceData(); |
---|
356 | |
---|
357 | Classification.executeUpdate( "DELETE FROM Classification c WHERE c.assaySample = ?", [this]) |
---|
358 | } |
---|
359 | |
---|
360 | /** |
---|
361 | * Recalculates the number of sequences for the given assaysample(s) |
---|
362 | */ |
---|
363 | public static void recalculateNumSequences( def selection = null ) { |
---|
364 | def whereClause = ""; |
---|
365 | def parameters = [:]; |
---|
366 | |
---|
367 | // Determine which samples to handle |
---|
368 | if( !selection ) { |
---|
369 | whereClause = ""; |
---|
370 | } else if( selection instanceof AssaySample ) { |
---|
371 | whereClause = "WHERE a = :selection "; |
---|
372 | parameters[ "selection" ] = selection; |
---|
373 | } else if( selection instanceof Collection ) { |
---|
374 | if( selection.findAll { it } ) { |
---|
375 | whereClause = "WHERE a IN (:selection) "; |
---|
376 | parameters[ "selection" ] = selection.findAll { it }; |
---|
377 | } |
---|
378 | } |
---|
379 | |
---|
380 | // Flush and clear session before updating |
---|
381 | def sessionFactory = AH.application.mainContext.sessionFactory; |
---|
382 | sessionFactory.getCurrentSession().flush(); |
---|
383 | sessionFactory.getCurrentSession().clear(); |
---|
384 | |
---|
385 | // Execute update query |
---|
386 | AssaySample.executeUpdate( "UPDATE AssaySample a SET a.numSequences = ( SELECT SUM( sd.numSequences ) FROM SequenceData sd WHERE sd.sample.id = a.id ) " + whereClause, parameters ); |
---|
387 | } |
---|
388 | |
---|
389 | /** |
---|
390 | * If an assaysample is used for communication to GSCF, the sample token is used. |
---|
391 | * @return |
---|
392 | */ |
---|
393 | public String token() { |
---|
394 | return sample.token(); |
---|
395 | } |
---|
396 | } |
---|