Several bias bugfixes
[amarok:rengelss-amarok.git] / src / dynamic / BiasSolver.cpp
1 /****************************************************************************************
2  * Copyright (c) 2008 Daniel Caleb Jones <danielcjones@gmail.com>                       *
3  * Copyright (c) 2010 Ralf Engels <ralf-engels@gmx.de>                                  *
4  *                                                                                      *
5  * This program is free software; you can redistribute it and/or modify it under        *
6  * the terms of the GNU General Public License as published by the Free Software        *
7  * Foundation; either version 2 of the License, or (at your option) version 3 or        *
8  * any later version accepted by the membership of KDE e.V. (or its successor approved  *
9  * by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of  *
10  * version 3 of the license.                                                            *
11  *                                                                                      *
12  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
13  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
14  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
15  *                                                                                      *
16  * You should have received a copy of the GNU General Public License along with         *
17  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
18  ****************************************************************************************/
19
20 #define DEBUG_PREFIX "BiasSolver"
21
22 #include "BiasSolver.h"
23 #include "core-impl/collections/support/CollectionManager.h"
24 #include "core/support/Debug.h"
25 #include "core/meta/support/MetaConstants.h"
26 #include "TrackSet.h"
27
28 #include <cmath>
29 #include <typeinfo>
30
31 #include <QHash>
32 #include <QMutexLocker>
33
34 #include <KRandom>
35
36
37 /* These number are black magic. The best values can only be obtained through
38  * exhaustive trial and error or writing another optimization program to
39  * optimize this optimization program. They are very sensitive. Be careful */
40 #include <threadweaver/Thread.h>
41
42 const int    Dynamic::BiasSolver::GA_ITERATION_LIMIT         = 70;
43 const int    Dynamic::BiasSolver::GA_POPULATION_SIZE         = 15;
44 const int    Dynamic::BiasSolver::GA_MATING_POPULATION_SIZE  = 5;
45 const double Dynamic::BiasSolver::GA_MUTATION_PROBABILITY    = 0.05;
46 const int    Dynamic::BiasSolver::GA_GIVE_UP_LIMIT           = 10;
47
48 const int    Dynamic::BiasSolver::SA_ITERATION_LIMIT     = 1000;
49 const double Dynamic::BiasSolver::SA_INITIAL_TEMPERATURE = 0.28;
50 const double Dynamic::BiasSolver::SA_COOLING_RATE        = 0.82;
51 const int    Dynamic::BiasSolver::SA_GIVE_UP_LIMIT       = 250;
52
53
54 namespace Dynamic
55 {
56     class SolverList
57     {
58         public:
59
60         SolverList( Meta::TrackList trackList,
61                     int contextCount,
62                     BiasPtr bias )
63             : m_trackList(trackList)
64             , m_contextCount( contextCount )
65             , m_bias( bias )
66             , m_energyValid( false )
67         {}
68
69         void appendTrack( Meta::TrackPtr track )
70         {
71             m_trackList.append( track );
72             m_energyValid = false;
73         }
74
75         void setTrack( int pos, Meta::TrackPtr track )
76         {
77             m_trackList.replace( pos, track );
78             m_energyValid = false;
79         }
80
81         bool operator<( const SolverList& x ) const
82         { return energy() < x.energy(); }
83
84         SolverList &operator=( const SolverList& x )
85         {
86             m_trackList = x.m_trackList;
87             m_energyValid = x.m_energyValid;
88             m_energy = x.m_energy;
89             m_contextCount = x.m_contextCount;
90
91             return *this;
92         }
93
94         double energy() const
95         {
96             if( !m_energyValid )
97             {
98                 m_energy = m_bias->energy( m_trackList, m_contextCount );
99                 m_energyValid = true;
100             }
101             return m_energy;
102         }
103
104         Meta::TrackList m_trackList;
105         int m_contextCount; // the number of tracks belonging to the context
106         BiasPtr m_bias;
107
108     private:
109         mutable bool m_energyValid;
110         mutable double m_energy;
111     };
112 }
113
114
115
116 Dynamic::BiasSolver::BiasSolver( int n, Dynamic::BiasPtr bias, Meta::TrackList context )
117     : m_n( n )
118     , m_bias( bias )
119     , m_context( context )
120     , m_abortRequested( false )
121 {
122     debug() << "CREATING BiasSolver in thread:" << QThread::currentThreadId() << "to get"<<n<<"tracks with"<<context.count()<<"context";
123     getTrackCollection();
124
125     connect( m_bias.data(), SIGNAL( resultReady( const Dynamic::TrackSet & ) ),
126              this,  SLOT( biasResultReady( const Dynamic::TrackSet & ) ) );
127 }
128
129
130 Dynamic::BiasSolver::~BiasSolver()
131 {
132     debug() << "DESTROYING BiasSolver in thread:" << QThread::currentThreadId();
133 }
134
135
136 void
137 Dynamic::BiasSolver::requestAbort()
138 {
139     m_abortRequested = true;
140 }
141
142 bool
143 Dynamic::BiasSolver::success() const
144 {
145     return !m_abortRequested;
146 }
147
148 void
149 Dynamic::BiasSolver::setAutoDelete( bool autoDelete )
150 {
151     if( autoDelete )
152     {
153         connect( this, SIGNAL( done( ThreadWeaver::Job* ) ), this, SLOT( deleteLater() ) );
154         connect( this, SIGNAL( failed( ThreadWeaver::Job* ) ), this, SLOT( deleteLater() ) );
155     }
156     else
157     {
158         disconnect( this, SIGNAL( done( ThreadWeaver::Job* ) ), this, SLOT( deleteLater() ) );
159         disconnect( this, SIGNAL( failed( ThreadWeaver::Job* ) ), this, SLOT( deleteLater() ) );
160     }
161 }
162
163
164 void Dynamic::BiasSolver::run()
165 {
166     DEBUG_BLOCK
167
168     debug() << "BiasSolver::run in thread:" << QThread::currentThreadId();
169
170     // wait until we get the track collection
171     {
172         QMutexLocker locker( &m_collectionResultsMutex );
173         if( !m_trackCollection )
174         {
175             debug() << "waiting for colleciton results";
176             m_collectionResultsReady.wait( &m_collectionResultsMutex );
177         }
178         debug() << "colleciton has" << m_trackCollection->count()<<"uids";
179     }
180
181     /*
182      * Two stage solver: Run ga_optimize and feed it's result into sa_optimize.
183      *
184      * Rationale: Genetic algorithms take better advantage of the heuristic used
185      * by generateInitialPlaylist and also have tendency to converge faster
186      * initially. How ever, they also tend to get stuck in local minima unless
187      * the population size is quite large, which is why we switch over to
188      * simulated annealing when that happens.
189      */
190
191     /*
192      * NOTE: For now I am disabling the ga phase, until I can do more
193      * experimentation.
194      */
195     //Meta::TrackList playlist = ga_optimize( GA_ITERATION_LIMIT, true );
196
197     debug() << "generating playlist";
198     SolverList playlist = generateInitialPlaylist();
199     debug() << "got playlist with"<<playlist.energy();
200     simpleOptimize( &playlist );
201     debug() << "after simple optimize playlist with"<<playlist.energy();
202     if( playlist.energy() > epsilon() && !m_abortRequested ) // the playlist is only slightly wrong
203     {
204        // debug...
205        for( int i = m_context.count();
206             i < playlist.m_contextCount + m_n && i < playlist.m_trackList.count(); i++ )
207        {
208            if( !m_bias->trackMatches( i, playlist.m_trackList, playlist.m_contextCount ) )
209                debug() << "track" << playlist.m_trackList[i]->name() << "does not match";
210        }
211
212        annealingOptimize( &playlist, SA_ITERATION_LIMIT, true );
213     }
214
215     m_solution = playlist.m_trackList.mid( m_context.count() );
216     debug() << "Found solution with energy"<<playlist.energy();
217 }
218
219 void
220 Dynamic::BiasSolver::simpleOptimize( SolverList *list )
221 {
222     DEBUG_BLOCK;
223
224     // TODO: don't optimize the tracks in order
225     TrackSet universeSet( m_trackCollection, true );
226     for( int i = list->m_contextCount;
227          i < list->m_contextCount + m_n && i < list->m_trackList.count(); i++ )
228     {
229         TrackSet set = matchingTracks( i, list->m_trackList );
230         Meta::TrackPtr newTrack = getRandomTrack( set );
231         if( newTrack )
232             list->setTrack( i, newTrack );
233     }
234 }
235
236 void
237 Dynamic::BiasSolver::annealingOptimize( SolverList *list,
238                                         int iterationLimit,
239                                         bool updateStatus )
240 {
241     DEBUG_BLOCK;
242
243     SolverList originalList = *list;
244
245     /*
246      * The process used here is called "simulated annealing". The basic idea is
247      * that the playlist is randomly mutated one track at a time. Mutations that
248      * improve the playlist (decrease the energy) are always accepted, mutations
249      * that make the playlist worse (increase the energy) are sometimes
250      * accepted. The decision to accept is made randomly based on a special
251      * probability curve that changes as the algorithm progresses.
252      *
253      * Accepting some bad mutations makes the algorithm resilient to getting
254      * stuck in local minima (playlists that are not optimal but can't be
255      * improved by making just one change). There is much more reading available
256      * on the internet or your local library.
257      */
258
259     double T = SA_INITIAL_TEMPERATURE;
260     TrackSet universeSet( m_trackCollection, true );
261
262     double oldEnergy = 0.0;
263     int giveUpCount = 0;
264     while( iterationLimit-- && list->energy() >= epsilon() && !m_abortRequested )
265     {
266         // if the energy hasn't changed in SA_GIVE_UP_LIMIT iterations, we give
267         // up and bail out.
268         if( oldEnergy == list->energy() )
269             giveUpCount++;
270         else
271         {
272             oldEnergy = list->energy();
273             giveUpCount = 0;
274         }
275
276         if( giveUpCount >= SA_GIVE_UP_LIMIT )
277             break;
278
279         // choose the mutation position
280         int newPos = (KRandom::random() % (list->m_trackList.count() - list->m_contextCount))
281             + list->m_contextCount;
282
283         // choose a matching track or a random one. Prefere matching
284         Meta::TrackPtr newTrack;
285         if( iterationLimit % 4 )
286         {
287             TrackSet set = matchingTracks( newPos, list->m_trackList );
288             newTrack = getRandomTrack( set );
289         }
290         else
291             newTrack = getRandomTrack( universeSet );
292
293         if( !newTrack )
294             continue;
295
296         debug() << "replacing"<<newPos<<list->m_trackList[newPos]->name()<<"with"<<newTrack->name();
297
298         SolverList newList = *list;
299         newList.setTrack( newPos, newTrack );
300
301         double p = 1.0 / ( 1.0 + exp( (newList.energy() - list->energy()) / list->m_trackList.count()  / T ) );
302         double r = (double)KRandom::random() / (((double)RAND_MAX) + 1.0);
303
304         // accept the mutation ?
305         if( r <= p )
306             *list = newList;
307
308         // cool the temperature
309         T *= SA_COOLING_RATE;
310
311         if( updateStatus && iterationLimit % 100 == 0 )
312         {
313             debug() << "SA: E = " << list->energy();
314             int progress = (int)(100.0 * (1.0 - list->energy()));
315             emit statusUpdate( progress >= 0 ? progress : 0 );
316         }
317     }
318
319     // -- use the original list if we made it worse
320     if( list->energy() > originalList.energy() )
321         *list = originalList;
322 }
323
324 void
325 Dynamic::BiasSolver::geneticOptimize( SolverList *list,
326                                       int iterationLimit,
327                                       bool updateStatus )
328 {
329     Q_UNUSED( list );
330     Q_UNUSED( iterationLimit );
331     Q_UNUSED( updateStatus );
332
333     /**
334      * Here we attempt to produce an optimal playlist using a genetic algorithm.
335      * The basic steps:
336      *
337      *   1. Generate a population of playlists using generateInitialPlaylist.
338      *
339      * REPEAT:
340      *   2. Choose a portion of that population to reproduce. The better the
341      *      playlist (the lower the energy) the more likely it is to reproduce.
342      *   3. The mating population playlists are mixed with each other producing
343      *      offspring playlists.
344      *   4. The worst playlists in the population are thrown out and replaced
345      *      with the new offspring.
346      */
347 #if 0
348
349     // 1.  Generate initial population
350     QList<SolverList> population;
351     SolverList playlist;
352     while( population.size() < GA_POPULATION_SIZE )
353     {
354         // TODO: OPTIMIZATION: most of the time spend solving now is spent
355         // getting Meta::Tracks, since we request so many with this. Experiment
356         // with lowering the population size, or finding a faster way to get a
357         // bunch of random tracks.
358         playlist = generateInitialPlaylist();
359
360         playlist.removeAll( Meta::TrackPtr() );
361
362         // test for the empty collection case
363         if( playlist.empty() )
364         {
365             warning() << "Empty collection, aborting.";
366             return Meta::TrackList();
367         }
368
369         double plEnergy = playlist->energy();
370
371         if( plEnergy < epsilon() ) // no need to continue if we already found an optimal playlist
372             return playlist;
373
374         population.append( playlist );
375     }
376
377     qSort( population ); // sort the population by energy.
378
379
380     double prevMin = 0.0;
381     int giveUpCount = 0;
382     int i = iterationLimit;
383     QList<int> matingPopulation;
384     while( i-- && population.first().energy >= epsilon() && !m_abortRequested )
385     {
386         // Sometime the optimal playlist can't have an energy of 0.0, or the
387         // algorithm just gets stuck. So if the energy hasn't changed after
388         // GIVE_UP_LIMIT iterations, we assume we bail out.
389         if( population.first().energy == prevMin )
390             giveUpCount++;
391         else
392         {
393             prevMin = population.first().energy;
394             giveUpCount = 0;
395         }
396
397         if( giveUpCount >= GA_GIVE_UP_LIMIT )
398             break;
399
400
401         // status updates
402         if( updateStatus && i % 5 == 0 )
403         {
404             int progress = (int)(100.0 * (1.0 - population.first().energy));
405             emit statusUpdate( progress >= 0 ? progress : 0 );
406         }
407
408         debug() << "GA: min E = " << population.first().energy;
409         debug() << "GA: max E = " << population.last().energy;
410
411
412
413         // 2. Choose the portion of the population to reproduce.
414         matingPopulation = generateMatingPopulation( population );
415
416         // randomize the order of the mating population so we don't get the same
417         // playlists mating over and over
418         int m = matingPopulation.size();
419         while( m > 1 )
420         {
421             int k = KRandom::random() % m;
422             --m;
423             matingPopulation.swap( m, k );
424         }
425
426         QList<Meta::TrackList> offspring;
427
428
429
430         // (I'm hanging on to code for now, until I do more testing.)
431         // reproduce using single point crossover
432 #if 0
433         for( int j = 0; j < matingPopulation.size(); ++j )
434         {
435             int parent1 = matingPopulation[j];
436             int parent2 = j == 0 ? matingPopulation.last() : matingPopulation[j-1];
437
438             Meta::TrackList child1, child2;
439             int locus = KRandom::random() % m_n;
440
441             child1 += population[parent1].trackList.mid( 0, locus );
442             child1 += population[parent2].trackList.mid( locus );
443
444             child2 += population[parent2].trackList.mid( 0, locus );
445             child2 += population[parent1].trackList.mid( locus );
446
447             offspring += child1;
448             offspring += child2;
449         }
450 #endif
451         
452         // 3. Reproduce (using uniform crossover).
453         for( int j = 0; j < matingPopulation.size(); ++j )
454         {
455             int parent1 = matingPopulation[j];
456             int parent2 = j == 0 ? matingPopulation.last() : matingPopulation[j-1];
457
458             Meta::TrackList child1, child2;
459
460             for( int k = 0; k < m_n; ++k )
461             {
462                 if( KRandom::random() < RAND_MAX/2 )
463                 {
464                     child1.append( population[parent1].trackList[k] );
465                     child2.append( population[parent2].trackList[k] );
466                 }
467                 else
468                 {
469                     child1.append( population[parent2].trackList[k] );
470                     child2.append( population[parent1].trackList[k] );
471                 }
472             }
473
474             offspring += child1;
475             offspring += child2;
476         }
477
478
479         // 4. Replace the worst in the population with the offspring.
480         int j = population.size() - 1;
481         foreach( const Meta::TrackList &p, offspring )
482         {
483             // TODO: try introducing mutations to the offspring here.
484
485             population[j--] = SolverList( p, energy(p) );
486         }
487
488         qSort( population ); // sort playlists by energy
489     }
490
491
492     // select the best solution
493     *list = population.first();
494 #endif
495 }
496
497
498 Meta::TrackList Dynamic::BiasSolver::solution()
499 {
500     return m_solution;
501 }
502
503
504 Dynamic::SolverList
505 Dynamic::BiasSolver::generateInitialPlaylist() const
506 {
507     SolverList result( m_context, m_context.count(), m_bias );
508
509     // Empty collection
510     if( m_trackCollection->count() == 0 )
511     {
512         debug() << "Empty collection when trying to generate initial playlist...";
513         return result;
514     }
515
516     // just create a simple playlist by adding random tracks to the end.
517
518     TrackSet universeSet( m_trackCollection, true );
519     while( result.m_trackList.count() < m_context.count() + m_n )
520     {
521         result.appendTrack( getRandomTrack( universeSet ) );
522     }
523
524     debug() << "generated random playlist with"<<result.m_trackList.count()<<"tracks";
525     return result;
526 }
527
528 Meta::TrackPtr
529 Dynamic::BiasSolver::getRandomTrack( const TrackSet& subset ) const
530 {
531     if( subset.trackCount() == 0 )
532         return Meta::TrackPtr();
533
534     Meta::TrackPtr track;
535
536     // this is really dumb, but we sometimes end up with uids that don't point to anything
537     int giveup = 50;
538     while( giveup-- && !track )
539         track = trackForUid( subset.getRandomTrack() );
540
541     if( track )
542     {
543         // if( track->artist() )
544             // debug() << "track selected:" << track->name() << track->artist()->name();
545     }
546     else
547         error() << "track is 0 in BiasSolver::getRandomTrack()";
548
549     return track;
550 }
551
552 Meta::TrackPtr
553 Dynamic::BiasSolver::trackForUid( const QString& uid ) const
554 {
555     const KUrl url( uid );
556     Meta::TrackPtr track = CollectionManager::instance()->trackForUrl( url );
557
558     if( !track )
559         warning() << "trackForUid returned no track for "<<uid;
560     return track;
561 }
562
563
564 // ---- getting the matchingTracks ----
565
566 void
567 Dynamic::BiasSolver::biasResultReady( const Dynamic::TrackSet &set )
568 {
569     DEBUG_BLOCK;
570     QMutexLocker locker( &m_biasResultsMutex );
571     m_tracks = set;
572     m_biasResultsReady.wakeAll();
573 }
574
575 Dynamic::TrackSet
576 Dynamic::BiasSolver::matchingTracks( int position, const Meta::TrackList& playlist ) const
577 {
578     QMutexLocker locker( &m_biasResultsMutex );
579     m_tracks = m_bias->matchingTracks( position, playlist, m_context.count(), m_trackCollection );
580     if( m_tracks.isOutstanding() )
581         m_biasResultsReady.wait( &m_biasResultsMutex );
582
583     debug() << "BiasSolver::matchingTracks returns"<<m_tracks.trackCount()<<"of"<<m_trackCollection->count()<<"tracks.";
584
585     return m_tracks;
586 }
587
588
589 // ---- getting the TrackCollection ----
590
591 void
592 Dynamic::BiasSolver::trackCollectionResultsReady( QString collectionId, QStringList uids )
593 {
594     Q_UNUSED( collectionId );
595     m_collectionUids.append( uids );
596 }
597
598 void
599 Dynamic::BiasSolver::trackCollectionDone()
600 {
601     QMutexLocker locker( &m_collectionResultsMutex );
602
603     m_trackCollection = TrackCollectionPtr( new TrackCollection( m_collectionUids ) );
604     m_collectionUids.clear();
605
606     m_collectionResultsReady.wakeAll();
607 }
608
609 void
610 Dynamic::BiasSolver::getTrackCollection()
611 {
612     // get all the unique ids from the collection manager
613     Collections::QueryMaker *qm = CollectionManager::instance()->queryMaker();
614     qm->setQueryType( Collections::QueryMaker::Custom );
615     qm->addReturnValue( Meta::valUniqueId );
616     qm->setAutoDelete( true );
617
618     connect( qm, SIGNAL(newResultReady( QString, QStringList )),
619              this, SLOT(trackCollectionResultsReady( QString, QStringList )),
620              Qt::DirectConnection );
621     connect( qm, SIGNAL(queryDone()),
622              this, SLOT(trackCollectionDone()),
623              Qt::DirectConnection );
624
625     qm->run();
626 }
627
628