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

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

Removed sql logs and solved small debugging bugs

File size: 10.4 KB
Line 
1package nl.tno.massSequencing
2
3import 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 */
11class 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}
Note: See TracBrowser for help on using the repository browser.