File indexing completed on 2024-05-05 04:48:47
0001 /**************************************************************************************** 0002 * Copyright (c) 2008-2012 Soren Harward <stharward@gmail.com> * 0003 * * 0004 * This program is free software; you can redistribute it and/or modify it under * 0005 * the terms of the GNU General Public License as published by the Free Software * 0006 * Foundation; either version 2 of the License, or (at your option) any later * 0007 * version. * 0008 * * 0009 * This program is distributed in the hope that it will be useful, but WITHOUT ANY * 0010 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * 0011 * PARTICULAR PURPOSE. See the GNU General Public License for more details. * 0012 * * 0013 * You should have received a copy of the GNU General Public License along with * 0014 * this program. If not, see <http://www.gnu.org/licenses/>. * 0015 ****************************************************************************************/ 0016 0017 #define DEBUG_PREFIX "APG::ConstraintSolver" 0018 0019 0020 #include "ConstraintSolver.h" 0021 0022 #include "Constraint.h" 0023 0024 #include "core/collections/QueryMaker.h" 0025 #include "core/meta/Meta.h" 0026 #include "core/support/Debug.h" 0027 #include "core/support/Components.h" 0028 #include "core/logger/Logger.h" 0029 #include "core-impl/collections/support/CollectionManager.h" 0030 #include "playlist/PlaylistModel.h" 0031 0032 #include <KLocalizedString> 0033 #include <QRandomGenerator> 0034 0035 #include <QHash> 0036 #include <QMutexLocker> 0037 #include <QRandomGenerator> 0038 #include <QStringList> 0039 #include <QTimer> 0040 #include <ThreadWeaver/ThreadWeaver> 0041 0042 #include <algorithm> // STL algorithms 0043 #include <cmath> 0044 #include <typeinfo> 0045 0046 const int APG::ConstraintSolver::QUALITY_RANGE = 10; 0047 0048 APG::ConstraintSolver::ConstraintSolver( ConstraintNode* r, int qualityFactor ) 0049 : QObject() 0050 , ThreadWeaver::Job() 0051 , m_satisfactionThreshold( 0.95 ) 0052 , m_finalSatisfaction( 0.0 ) 0053 , m_constraintTreeRoot( r ) 0054 , m_domainReductionFailed( false ) 0055 , m_readyToRun( false ) 0056 , m_abortRequested( false ) 0057 , m_maxGenerations( 100 ) 0058 , m_populationSize( 40 ) 0059 , m_suggestedPlaylistSize( 15 ) 0060 { 0061 Q_UNUSED( qualityFactor); // FIXME 0062 0063 m_serialNumber = QRandomGenerator::global()->generate(); 0064 0065 if ( !m_constraintTreeRoot ) { 0066 error() << "No constraint tree was passed to the solver. Aborting."; 0067 m_readyToRun = true; 0068 m_abortRequested = true; 0069 return; 0070 } 0071 0072 m_qm = CollectionManager::instance()->queryMaker(); 0073 if ( m_qm ) { 0074 debug() << "New ConstraintSolver with serial number" << m_serialNumber; 0075 m_qm->setQueryType( Collections::QueryMaker::Track ); 0076 connect( m_qm, &Collections::QueryMaker::newTracksReady, 0077 this, &APG::ConstraintSolver::receiveQueryMakerData, Qt::QueuedConnection ); 0078 connect( m_qm, &Collections::QueryMaker::queryDone, 0079 this, &APG::ConstraintSolver::receiveQueryMakerDone, Qt::QueuedConnection ); 0080 m_constraintTreeRoot->initQueryMaker( m_qm ); 0081 m_qm->run(); 0082 } else { 0083 debug() << "The ConstraintSolver could not find any queryable collections. No results will be returned."; 0084 m_readyToRun = true; 0085 m_abortRequested = true; 0086 } 0087 } 0088 0089 APG::ConstraintSolver::~ConstraintSolver() 0090 { 0091 if ( m_qm ) { 0092 m_qm->abortQuery(); 0093 m_qm->deleteLater(); 0094 m_qm = nullptr; 0095 } 0096 } 0097 0098 Meta::TrackList 0099 APG::ConstraintSolver::getSolution() const 0100 { 0101 return m_solvedPlaylist; 0102 } 0103 0104 bool 0105 APG::ConstraintSolver::satisfied() const 0106 { 0107 return m_finalSatisfaction > m_satisfactionThreshold; 0108 } 0109 0110 bool 0111 APG::ConstraintSolver::canBeExecuted() 0112 { 0113 0114 /* This is a hopefully superfluous check to ensure that the Solver job 0115 * doesn't get run until it's ready (ie, when QueryMaker has finished). 0116 * This shouldn't ever return false, because hopefully the job won't even 0117 * get queued until it's ready to run. See the comments in 0118 * Preset::queueSolver() for more information. -- sth */ 0119 0120 return m_readyToRun; 0121 } 0122 0123 void 0124 APG::ConstraintSolver::requestAbort() 0125 { 0126 if ( m_qm ) { 0127 m_qm->abortQuery(); 0128 m_qm->deleteLater(); 0129 m_qm = nullptr; 0130 } 0131 m_abortRequested = true; 0132 } 0133 0134 bool 0135 APG::ConstraintSolver::success() const 0136 { 0137 return !m_abortRequested; 0138 } 0139 0140 void 0141 APG::ConstraintSolver::run(ThreadWeaver::JobPointer self, ThreadWeaver::Thread *thread) 0142 { 0143 Q_UNUSED(self); 0144 Q_UNUSED(thread); 0145 0146 if ( !m_readyToRun ) { 0147 error() << "DANGER WILL ROBINSON! A ConstraintSolver (serial no:" << m_serialNumber << ") tried to run before its QueryMaker finished!"; 0148 m_abortRequested = true; 0149 return; 0150 } 0151 0152 if ( m_domain.empty() ) { 0153 debug() << "The QueryMaker returned no tracks"; 0154 return; 0155 } else { 0156 debug() << "Domain has" << m_domain.size() << "tracks"; 0157 } 0158 0159 debug() << "Running ConstraintSolver" << m_serialNumber; 0160 0161 Q_EMIT totalSteps( m_maxGenerations ); 0162 0163 // GENETIC ALGORITHM LOOP 0164 Population population; 0165 quint32 generation = 0; 0166 Meta::TrackList* best = nullptr; 0167 while ( !m_abortRequested && ( generation < m_maxGenerations ) ) { 0168 quint32 s = m_constraintTreeRoot->suggestPlaylistSize(); 0169 m_suggestedPlaylistSize = (s > 0) ? s : m_suggestedPlaylistSize; 0170 fill_population( population ); 0171 best = find_best( population ); 0172 if ( population.value( best ) < m_satisfactionThreshold ) { 0173 select_population( population, best ); 0174 mutate_population( population ); 0175 generation++; 0176 Q_EMIT incrementProgress(); 0177 } else { 0178 break; 0179 } 0180 } 0181 0182 if( best ) 0183 { 0184 debug() << "solution at" << (void*)(best); 0185 0186 m_solvedPlaylist = best->mid( 0 ); 0187 m_finalSatisfaction = m_constraintTreeRoot->satisfaction( m_solvedPlaylist ); 0188 } 0189 0190 /* clean up */ 0191 Population::iterator it = population.begin(); 0192 while ( it != population.end() ) { 0193 delete it.key(); 0194 it = population.erase( it ); 0195 } 0196 0197 Q_EMIT endProgressOperation( this ); 0198 } 0199 0200 void APG::ConstraintSolver::defaultBegin(const ThreadWeaver::JobPointer& self, ThreadWeaver::Thread *thread) 0201 { 0202 Q_EMIT started(self); 0203 ThreadWeaver::Job::defaultBegin(self, thread); 0204 } 0205 0206 void APG::ConstraintSolver::defaultEnd(const ThreadWeaver::JobPointer& self, ThreadWeaver::Thread *thread) 0207 { 0208 ThreadWeaver::Job::defaultEnd(self, thread); 0209 if (!self->success()) { 0210 Q_EMIT failed(self); 0211 } 0212 Q_EMIT done(self); 0213 } 0214 0215 void 0216 APG::ConstraintSolver::receiveQueryMakerData( const Meta::TrackList &results ) 0217 { 0218 m_domainMutex.lock(); 0219 m_domain += results; 0220 m_domainMutex.unlock(); 0221 } 0222 0223 void 0224 APG::ConstraintSolver::receiveQueryMakerDone() 0225 { 0226 m_qm->deleteLater(); 0227 m_qm = nullptr; 0228 0229 if (( m_domain.size() > 0 ) || m_domainReductionFailed ) { 0230 if ( m_domain.size() <= 0 ) { 0231 Amarok::Logger::shortMessage( i18n("The playlist generator failed to load any tracks from the collection.") ); 0232 } 0233 m_readyToRun = true; 0234 Q_EMIT readyToRun(); 0235 } else { 0236 Amarok::Logger::longMessage( 0237 i18n("There are no tracks that match all constraints. " \ 0238 "The playlist generator will find the tracks that match best, " \ 0239 "but you may want to consider loosening the constraints to find more tracks.") ); 0240 m_domainReductionFailed = true; 0241 0242 // need a new query maker without constraints 0243 m_qm = CollectionManager::instance()->queryMaker(); 0244 if ( m_qm ) { 0245 connect( m_qm, &Collections::QueryMaker::newTracksReady, 0246 this, &APG::ConstraintSolver::receiveQueryMakerData, Qt::QueuedConnection ); 0247 connect( m_qm, &Collections::QueryMaker::queryDone, 0248 this, &APG::ConstraintSolver::receiveQueryMakerDone, Qt::QueuedConnection ); 0249 0250 m_qm->setQueryType( Collections::QueryMaker::Track ); 0251 m_qm->run(); 0252 } 0253 } 0254 } 0255 0256 void 0257 APG::ConstraintSolver::fill_population( Population& population ) 0258 { 0259 for ( int i = population.size(); quint32(i) < m_populationSize; i++ ) { 0260 Meta::TrackList* tl = new Meta::TrackList( sample( m_domain, playlist_size() ) ); 0261 double s = m_constraintTreeRoot->satisfaction( (*tl) ); 0262 population.insert( tl, s ); 0263 } 0264 } 0265 0266 Meta::TrackList* APG::ConstraintSolver::find_best(const APG::ConstraintSolver::Population& population ) const 0267 { 0268 Population::const_iterator it = std::max_element( population.constBegin(), population.constEnd(), &pop_comp ); 0269 return it.key(); 0270 } 0271 0272 void 0273 APG::ConstraintSolver::select_population( APG::ConstraintSolver::Population& population, Meta::TrackList* best ) 0274 { 0275 Population::Iterator it = population.begin(); 0276 while ( it != population.end() ) { 0277 if ( it.key() == best ) { 0278 ++it;// Always keep the best solution, no matter how bad it is 0279 if ( it == population.end() ) 0280 break; 0281 } 0282 0283 if ( select( it.value() ) ) { 0284 ++it; 0285 } else { 0286 delete it.key(); 0287 it = population.erase( it ); 0288 } 0289 } 0290 } 0291 0292 void 0293 APG::ConstraintSolver::mutate_population( APG::ConstraintSolver::Population& population ) 0294 { 0295 if ( population.size() < 1 ) 0296 return; 0297 0298 const double mutantPercentage = 0.35; // TODO: tune this parameter 0299 0300 QList<Meta::TrackList*> parents( population.keys() ); 0301 int maxMutants = (int)( mutantPercentage * (double)(m_populationSize) ); 0302 for ( int i = parents.size(); i < maxMutants; i++ ) { 0303 int idx = QRandomGenerator::global()->generate() % parents.size(); 0304 Meta::TrackList* child = new Meta::TrackList( *(parents.at( idx )) ); 0305 int op = QRandomGenerator::global()->generate() % 5; 0306 int s = child->size(); 0307 switch (op) { 0308 case 0: 0309 child->removeAt( QRandomGenerator::global()->generate() % s ); 0310 break; 0311 case 1: 0312 child->insert( QRandomGenerator::global()->generate() % ( s + 1 ), random_track_from_domain() ); 0313 break; 0314 case 2: 0315 child->replace( QRandomGenerator::global()->generate() % s, random_track_from_domain() ); 0316 break; 0317 case 3: 0318 child->swapItemsAt( QRandomGenerator::global()->generate() % s, QRandomGenerator::global()->generate() % s ); 0319 break; 0320 case 4: 0321 child = crossover( child, parents.at( QRandomGenerator::global()->generate() % parents.size() ) ); 0322 break; 0323 default: 0324 (void)0; // effectively a no-op. the default is here so that the compiler doesn't complain about missing default in switch 0325 } 0326 population.insert( child, m_constraintTreeRoot->satisfaction( *child ) ); 0327 } 0328 return; 0329 } 0330 0331 Meta::TrackList* 0332 APG::ConstraintSolver::crossover( Meta::TrackList* top, Meta::TrackList* bot ) const 0333 { 0334 const double crossoverPt = 0.5; // TODO: choose different values 0335 0336 int topV = (int)( crossoverPt * (double)top->size() ); 0337 int botV = (int)( crossoverPt * (double)bot->size() ); 0338 0339 Meta::TrackList* newlist = new Meta::TrackList( top->mid( 0, topV ) ); 0340 newlist->append( bot->mid( botV ) ); 0341 0342 delete top; 0343 return newlist; 0344 } 0345 0346 bool 0347 APG::ConstraintSolver::pop_comp( double a, double b ) 0348 { 0349 return ( a < b ); 0350 } 0351 0352 Meta::TrackPtr 0353 APG::ConstraintSolver::random_track_from_domain() const 0354 { 0355 return m_domain.at( QRandomGenerator::global()->generate() % m_domain.size() ); 0356 } 0357 0358 Meta::TrackList 0359 APG::ConstraintSolver::sample( Meta::TrackList domain, const int sampleSize ) const 0360 { 0361 std::random_shuffle( domain.begin(), domain.end() ); 0362 return domain.mid( 0, sampleSize ); 0363 } 0364 0365 quint32 0366 APG::ConstraintSolver::playlist_size() const 0367 { 0368 return rng_poisson( (double)m_suggestedPlaylistSize ); 0369 } 0370 0371 bool 0372 APG::ConstraintSolver::select( const double satisfaction ) const 0373 { 0374 double x = (double)QRandomGenerator::global()->generate()/(double)RAND_MAX; 0375 const double scale = -30.0; // TODO: make adjustable 0376 return ( x < 1.0 / ( 1.0 + exp( scale * (satisfaction-0.8) ) ) ); 0377 } 0378 0379 void 0380 APG::ConstraintSolver::dump_population( const Population& population ) const 0381 { 0382 DEBUG_BLOCK 0383 for ( Population::ConstIterator it = population.constBegin(); it != population.constEnd(); ++it ) { 0384 Meta::TrackList* tl = it.key(); 0385 debug() << "at" << (void*)(tl) << "satisfaction:" << it.value(); 0386 foreach ( Meta::TrackPtr t, (*tl) ) { 0387 debug() << "\ttrack:" << t->prettyName(); 0388 } 0389 } 0390 } 0391 0392 double 0393 APG::ConstraintSolver::rng_gaussian( const double mu, const double sigma ) const 0394 { 0395 /* adapted from randist/gauss.c in GNU Scientific Library 1.14 */ 0396 double u, v, x, y, Q; 0397 const double s = 0.449871; 0398 const double t = -0.386595; 0399 const double a = 0.19600; 0400 const double b = 0.25472; 0401 const double r1 = 0.27597; 0402 const double r2 = 0.27846; 0403 0404 do { 0405 u = 1 - rng_uniform(); 0406 v = ( rng_uniform() - 0.5 ) * 1.7156; 0407 x = u - s; 0408 y = fabs (v) - t; 0409 Q = x * x + y * (a * y - b * x); 0410 } while (Q >= r1 && (Q > r2 || v * v > -4 * u * u * log (u))); 0411 0412 return mu + ( sigma * (v / u) ); 0413 } 0414 0415 quint32 0416 APG::ConstraintSolver::rng_poisson( const double mu ) const 0417 { 0418 if ( mu >= 25.0 ) { 0419 double v = rng_gaussian( mu, sqrt( mu ) ); 0420 return ( v < 0.0 ) ? 0 : (quint32)v; 0421 } 0422 0423 const double emu = exp( -mu ); 0424 double prod = 1.0; 0425 quint32 k = 0; 0426 0427 do { 0428 prod *= rng_uniform(); 0429 k++; 0430 } 0431 while ( prod > emu ); 0432 0433 return k - 1; 0434 } 0435 0436 double 0437 APG::ConstraintSolver::rng_uniform() const 0438 { 0439 return ( (double)QRandomGenerator::global()->generate() / (double)(RAND_MAX) ); 0440 }