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
RevLine 
[28]1package nl.tno.massSequencing
[2]2
3/**
4 * Represents a samples that is used in an assay.
5 *
6 * @author Robert Horlings (robert@isdat.nl)
7 *
8 */
9class AssaySample {
[52]10        // Grails datasource is used for executing custom SQL statement
11        def dataSource
12       
13        // To be computed at run time
[2]14        private long _numSequences = -1;
15        private float _averageQuality = -1.0;
[12]16        private long _numQualScores = -1;
[52]17        private long _numSequenceFiles = -1;
18        private long _numQualityFiles = -1;
19       
[2]20        Integer numUniqueSequences      // Number of unique sequences / OTUs. Is only available after preprocessing
21
[24]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
[2]33
[7]34        static belongsTo  = [ assay: Assay, sample: Sample, run: Run ]
[2]35        static hasMany    = [ sequenceData: SequenceData ]
36
37        static constraints = {
38                numUniqueSequences(nullable: true)
[24]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)
[7]49                run(nullable: true);
[2]50        }
51
52        static mapping = {
53                columns {
54                        numSequences index:'numsequences_idx'
55                }
[7]56                sequenceData cascade: "all-delete-orphan"
[52]57                sample fetch: 'join'
58                assay fetch: 'join'
59                run fetch: 'join'
[2]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() {
[52]69                return numSequenceFiles() + numQualityFiles(); 
[2]70        }
71
72        /**
[58]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        /**
[3]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() {
[52]87                if( _numSequenceFiles > -1 )
88                        return _numSequenceFiles;
89
[3]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() {
[52]109                if( _numQualityFiles > -1 )
110                        return _numQualityFiles;
111               
[3]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        /**
[2]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;
[3]133
[2]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        }
[12]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;
[2]155
[12]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        }
[2]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;
[3]190
[2]191                return averageQuality;
192        }
[3]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;
[12]199                _numQualScores = -1;
[3]200                _averageQuality = -1;
[52]201               
202                _numSequenceFiles = -1;
203                _numQualityFiles = -1;
[3]204        }
[4]205       
206        /**
[52]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        /**
[4]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() {
[24]268                return  fwOligo || fwMidName || fwTotalSeq || fwMidSeq || fwPrimerSeq ||
269                                revOligo || revMidName || revTotalSeq || revMidSeq || revPrimerSeq ||
[58]270                                numFiles() > 0 || numClassifiedSequences() > 0;
[4]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
[24]282                otherAssaySample.fwOligo                = fwOligo;
283                otherAssaySample.fwMidName              = fwMidName;
284                otherAssaySample.fwTotalSeq     = fwTotalSeq;
285                otherAssaySample.fwMidSeq               = fwMidSeq;
286                otherAssaySample.fwPrimerSeq    = fwPrimerSeq;
[4]287               
[24]288                otherAssaySample.revOligo               = revOligo;
289                otherAssaySample.revMidName             = revMidName;
290                otherAssaySample.revTotalSeq    = revTotalSeq;
291                otherAssaySample.revMidSeq              = revMidSeq;
292                otherAssaySample.revPrimerSeq   = revPrimerSeq;
293
[4]294                // Move attached data
[20]295                def dataList = [] + sequenceData?.toList()
[4]296                def otherAssay = otherAssaySample.assay;
297               
298                if( dataList && dataList.size() > 0 ) {
299                        for( def j = dataList.size() - 1; j >= 0; j-- ) {
[20]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                               
[28]304                                if( dataList[ j ] ) {
305                                        // Clone the sequencedata object
306                                        def sd = dataList[ j ]?.clone();
[20]307                                       
[28]308                                        if( sd )
309                                                otherAssaySample.addToSequenceData( sd );
310                                               
311                                        // Remove the old sequencedata object
312                                        this.removeFromSequenceData( dataList[ j ] );
313                                }
[4]314                        }
315                }
[58]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       
[20]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                }
[4]333        }
[49]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
[58]353                // Remove all sequence objects referencing this sequenceData object
354                Sequence.executeUpdate( "DELETE FROM Sequence s WHERE s.assaySample = ?", [this])
355               
[49]356                resetStats();
357                save();
358               
359                return numFiles;
360        }
361
[58]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        }
[2]371}
Note: See TracBrowser for help on using the repository browser.