source: trunk/grails-app/domain/nl/tno/massSequencing/AssaySample.groovy @ 58

Last change on this file since 58 was 58, checked in by robert@…, 8 years ago

Implemented importing of classifications

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