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

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

Last fixes and extra bugfix in import controller

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