File indexing completed on 2024-05-19 04:49:41
0001 /**************************************************************************************** 0002 * Copyright (c) 2011 Ralf Engels <ralf-engels@gmx.de> * 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) version 3 or * 0007 * any later version accepted by the membership of KDE e.V. (or its successor approved * 0008 * by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of * 0009 * version 3 of the license. * 0010 * * 0011 * This program is distributed in the hope that it will be useful, but WITHOUT ANY * 0012 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * 0013 * PARTICULAR PURPOSE. See the GNU General Public License for more details. * 0014 * * 0015 * You should have received a copy of the GNU General Public License along with * 0016 * this program. If not, see <http://www.gnu.org/licenses/>. * 0017 ****************************************************************************************/ 0018 0019 #define DEBUG_PREFIX "PartBias" 0020 0021 #include "PartBias.h" 0022 0023 #include "core/meta/Meta.h" 0024 #include "core/support/Debug.h" 0025 #include "widgets/SliderWidget.h" 0026 0027 #include <KLocalizedString> 0028 #include <QRandomGenerator> 0029 0030 #include <QtGlobal> // for qRound 0031 #include <QApplication> 0032 #include <QGridLayout> 0033 #include <QLabel> 0034 #include <QSlider> 0035 #include <QStyle> 0036 #include <QStyleOption> 0037 #include <QWidget> 0038 #include <QXmlStreamReader> 0039 #include <QXmlStreamWriter> 0040 0041 QString 0042 Dynamic::PartBiasFactory::i18nName() const 0043 { return i18nc("Name of the \"Part\" bias", "Partition"); } 0044 0045 QString 0046 Dynamic::PartBiasFactory::name() const 0047 { return Dynamic::PartBias::sName(); } 0048 0049 QString 0050 Dynamic::PartBiasFactory::i18nDescription() const 0051 { return i18nc("Description of the \"Part\" bias", 0052 "The \"Part\" bias fills parts of the playlist from different sub-biases."); } 0053 0054 Dynamic::BiasPtr 0055 Dynamic::PartBiasFactory::createBias() 0056 { return Dynamic::BiasPtr( new Dynamic::PartBias() ); } 0057 0058 0059 0060 /* Note: 0061 We use the Ford-Fulkerson method to compute the maximum match between the bias 0062 and the tracks. 0063 The MatchState class will do this matching and keep track of all the needed 0064 data. 0065 We are not building up the full graph and we don't even compute in advance 0066 all the edges. 0067 */ 0068 0069 /** This is the helper object to calculate the maximum match. 0070 For the sake of the algorithm we are using every sub-bias as a source with 0071 a capacity depending on it's weight. 0072 Every track in the playlist is a drain with capacity 1. 0073 */ 0074 class MatchState 0075 { 0076 public: 0077 /** Creates the matching 0078 */ 0079 MatchState( const Dynamic::PartBias *bias, 0080 const Meta::TrackList& playlist, 0081 int contextCount, int finalCount ) 0082 : m_bias( bias ) 0083 , m_playlist( playlist ) 0084 , m_contextCount( contextCount ) 0085 , m_sourceCount( bias->weights().count() ) 0086 , m_drainCount( finalCount - contextCount ) 0087 , m_edges( m_sourceCount * m_drainCount, false ) 0088 , m_edgesUsed( m_sourceCount * m_drainCount, false ) 0089 , m_sourceCapacity( m_sourceCount ) 0090 , m_sourceFlow( m_sourceCount ) 0091 , m_drainFlow( m_drainCount ) 0092 , m_drainSource( m_drainCount ) 0093 { 0094 QList<qreal> weights = m_bias->weights(); 0095 0096 int assignedDrainCount = 0; 0097 for( int source = 0; source < m_sourceCount-1; source++ ) 0098 { 0099 m_sourceCapacity[source] = qRound( weights[source] * m_drainCount ); 0100 assignedDrainCount += m_sourceCapacity[source]; 0101 // debug() << "MatchState: bias"<<m_bias->biases()[source]->name()<<"should match"<<m_sourceCapacity[source]<<"of"<< m_drainCount << "tracks."; 0102 } 0103 0104 // the last bias get's all the rest 0105 if( m_sourceCount > 0 ) 0106 m_sourceCapacity[m_sourceCount - 1] = m_drainCount - assignedDrainCount; 0107 0108 compute(); 0109 } 0110 0111 void compute() 0112 { 0113 // -- initialize the values 0114 for( int source = m_sourceCount-1; source >= 0; --source ) 0115 m_sourceFlow[source] = 0; 0116 0117 for( int drain = m_drainCount-1; drain >= 0; --drain ) 0118 { 0119 m_drainFlow[drain] = 0; 0120 m_drainSource[drain] = -1; 0121 } 0122 0123 // -- get all the edges 0124 Dynamic::BiasList biases = m_bias->biases(); 0125 for( int source = m_sourceCount-1; source >= 0; --source ) 0126 for( int drain = m_drainCount-1; drain >= 0; --drain ) 0127 { 0128 m_edgesUsed[ source * m_drainCount + drain ] = false; 0129 0130 if( drain + m_contextCount >= m_playlist.count() ) 0131 continue; 0132 0133 m_edges[ source * m_drainCount + drain ] = 0134 biases[source]->trackMatches( drain + m_contextCount, 0135 m_playlist, 0136 m_contextCount ); 0137 // debug() << "edge:" << source << "x" << drain << ":" << m_edges[ source * m_drainCount + drain ]; 0138 } 0139 0140 // find a source whose capacity is not full 0141 for( int source = m_sourceCount-1; source >= 0; --source ) 0142 { 0143 if( m_sourceFlow[source] >= m_sourceCapacity[source] ) 0144 continue; 0145 0146 for( int drain = 0; drain < m_drainCount; drain++ ) 0147 { 0148 if( !m_edges[ source * m_drainCount + drain ] ) 0149 continue; 0150 0151 if( m_drainFlow[drain] < 1 ) 0152 { 0153 // direct connections source to drain 0154 // make a new connection 0155 m_sourceFlow[source]++; 0156 m_drainFlow[drain]++; 0157 m_drainSource[drain] = source; 0158 m_edgesUsed[ source * m_drainCount + drain ] = true; 0159 } 0160 else 0161 { 0162 // indirect connections source to drain to source to drain 0163 // or in other words: Check if we can re-order another source 0164 // to get a connection for this source 0165 int source2 = m_drainSource[drain]; 0166 0167 for( int drain2 = m_drainCount-1; drain2 >= 0; --drain2 ) 0168 { 0169 if( m_drainFlow[drain2] > 0 ) 0170 continue; 0171 if( !m_edgesUsed[ source2 * m_drainCount + drain ] ) 0172 continue; 0173 if( !m_edges[ source2 * m_drainCount + drain2 ] ) 0174 continue; 0175 0176 // revert the old connection 0177 m_sourceFlow[source2]--; 0178 m_drainFlow[drain]--; 0179 m_edgesUsed[ source2 * m_drainCount + drain ] = false; 0180 0181 // make two new connections 0182 m_sourceFlow[source]++; 0183 m_drainFlow[drain]++; 0184 m_drainSource[drain] = source; 0185 m_edgesUsed[ source * m_drainCount + drain ] = true; 0186 0187 m_sourceFlow[source2]++; 0188 m_drainFlow[drain2]++; 0189 m_drainSource[drain2] = source2; 0190 m_edgesUsed[ source2 * m_drainCount + drain2 ] = true; 0191 break; 0192 } 0193 0194 } 0195 0196 // finished with this source? 0197 if( m_sourceFlow[source] >= m_sourceCapacity[source] ) 0198 break; 0199 } 0200 } 0201 } 0202 0203 0204 const Dynamic::PartBias* const m_bias; 0205 const Meta::TrackList& m_playlist; 0206 int m_contextCount; 0207 0208 int m_sourceCount; 0209 int m_drainCount; 0210 QBitArray m_edges; 0211 QBitArray m_edgesUsed; 0212 0213 QVector<int> m_sourceCapacity; 0214 QVector<int> m_sourceFlow; 0215 QVector<int> m_drainFlow; 0216 QVector<int> m_drainSource; // the source currently used by the drain 0217 }; 0218 0219 // -------- PartBiasWidget ----------- 0220 0221 Dynamic::PartBiasWidget::PartBiasWidget( Dynamic::PartBias* bias, QWidget* parent ) 0222 : QWidget( parent ) 0223 , m_inSignal( false ) 0224 , m_bias( bias ) 0225 { 0226 connect( bias, &PartBias::biasAppended, 0227 this, &PartBiasWidget::biasAppended ); 0228 0229 connect( bias, &PartBias::biasRemoved, 0230 this, &PartBiasWidget::biasRemoved ); 0231 0232 connect( bias, &PartBias::biasMoved, 0233 this, &PartBiasWidget::biasMoved ); 0234 0235 connect( bias, &PartBias::weightsChanged, 0236 this, &PartBiasWidget::biasWeightsChanged ); 0237 0238 m_layout = new QGridLayout( this ); 0239 0240 // -- add all sub-bias widgets 0241 foreach( Dynamic::BiasPtr bias, m_bias->biases() ) 0242 { 0243 biasAppended( bias ); 0244 } 0245 } 0246 0247 void 0248 Dynamic::PartBiasWidget::biasAppended( Dynamic::BiasPtr bias ) 0249 { 0250 int index = m_bias->biases().indexOf( bias ); 0251 0252 Amarok::Slider* slider = nullptr; 0253 slider = new Amarok::Slider( Qt::Horizontal, 100 ); 0254 slider->setValue( m_bias->weights()[ m_bias->biases().indexOf( bias ) ] * 100.0 ); 0255 slider->setToolTip( i18n( "This controls what portion of the playlist should match the criteria" ) ); 0256 connect( slider, &Amarok::Slider::valueChanged, this, &PartBiasWidget::sliderValueChanged ); 0257 0258 QLabel* label = new QLabel( bias->toString() ); 0259 0260 m_sliders.append( slider ); 0261 m_widgets.append( label ); 0262 // -- add the widget (with slider) 0263 m_layout->addWidget( slider, index, 0 ); 0264 m_layout->addWidget( label, index, 1 ); 0265 } 0266 0267 void 0268 Dynamic::PartBiasWidget::biasRemoved( int pos ) 0269 { 0270 m_layout->takeAt( pos * 2 ); 0271 m_layout->takeAt( pos * 2 ); 0272 m_sliders.takeAt( pos )->deleteLater(); 0273 m_widgets.takeAt( pos )->deleteLater(); 0274 } 0275 0276 void 0277 Dynamic::PartBiasWidget::biasMoved( int from, int to ) 0278 { 0279 QSlider* slider = m_sliders.takeAt( from ); 0280 m_sliders.insert( to, slider ); 0281 0282 QWidget* widget = m_widgets.takeAt( from ); 0283 m_widgets.insert( to, widget ); 0284 0285 // -- move the item in the layout 0286 // TODO 0287 /* 0288 m_layout->insertWidget( to * 2, slider ); 0289 m_layout->insertWidget( to * 2 + 1, widget ); 0290 */ 0291 } 0292 0293 void 0294 Dynamic::PartBiasWidget::sliderValueChanged( int val ) 0295 { 0296 DEBUG_BLOCK; 0297 // protect against recursion 0298 if( m_inSignal ) 0299 return; 0300 0301 for( int i = 0; i < m_sliders.count(); i++ ) 0302 { 0303 if( m_sliders.at(i) == sender() ) 0304 m_bias->changeBiasWeight( i, qreal(val) / 100.0 ); 0305 } 0306 } 0307 0308 void 0309 Dynamic::PartBiasWidget::biasWeightsChanged() 0310 { 0311 DEBUG_BLOCK; 0312 // protect against recursion 0313 if( m_inSignal ) 0314 return; 0315 0316 m_inSignal = true; 0317 0318 QList<qreal> weights = m_bias->weights(); 0319 for( int i = 0; i < weights.count() && i < m_sliders.count(); i++ ) 0320 m_sliders.at(i)->setValue( weights.at(i) * 100.0 ); 0321 0322 m_inSignal = false; 0323 } 0324 0325 0326 0327 // ----------- PartBias ---------------- 0328 0329 Dynamic::PartBias::PartBias() 0330 : AndBias() 0331 { 0332 // add weights for already existing biases 0333 for( int i = 0; i < biases().count(); i++ ) 0334 m_weights.append( 1.0 / biases().count() ); 0335 } 0336 0337 void 0338 Dynamic::PartBias::fromXml( QXmlStreamReader *reader ) 0339 { 0340 QList<qreal> weights; // we have to add all biases before we can set their weights 0341 0342 while (!reader->atEnd()) { 0343 reader->readNext(); 0344 0345 if( reader->isStartElement() ) 0346 { 0347 float weight = reader->attributes().value( QStringLiteral("weight") ).toString().toFloat(); 0348 Dynamic::BiasPtr bias( Dynamic::BiasFactory::fromXml( reader ) ); 0349 if( bias ) 0350 { 0351 appendBias( bias ); 0352 weights.append( weight ); 0353 } 0354 else 0355 { 0356 warning()<<"Unexpected xml start element"<<reader->name()<<"in input"; 0357 reader->skipCurrentElement(); 0358 } 0359 } 0360 else if( reader->isEndElement() ) 0361 { 0362 break; 0363 } 0364 } 0365 0366 m_weights = weights; 0367 } 0368 0369 void 0370 Dynamic::PartBias::toXml( QXmlStreamWriter *writer ) const 0371 { 0372 for( int i = 0; i < m_biases.count(); i++ ) 0373 { 0374 writer->writeStartElement( m_biases[i]->name() ); 0375 writer->writeAttribute( QStringLiteral("weight"), QString::number(m_weights[i]) ); 0376 m_biases[i]->toXml( writer ); 0377 writer->writeEndElement(); 0378 } 0379 } 0380 0381 QString 0382 Dynamic::PartBias::sName() 0383 { 0384 return QStringLiteral( "partBias" ); 0385 } 0386 0387 QString 0388 Dynamic::PartBias::name() const 0389 { 0390 return Dynamic::PartBias::sName(); 0391 } 0392 0393 QString 0394 Dynamic::PartBias::toString() const 0395 { 0396 return i18nc("Part bias representation", "Partition"); 0397 } 0398 0399 0400 QWidget* 0401 Dynamic::PartBias::widget( QWidget* parent ) 0402 { 0403 return new Dynamic::PartBiasWidget( this, parent ); 0404 } 0405 0406 void 0407 Dynamic::PartBias::paintOperator( QPainter* painter, const QRect& rect, Dynamic::AbstractBias* bias ) 0408 { 0409 int index = m_biases.indexOf( Dynamic::BiasPtr(bias) ); 0410 if( index < 0 ) 0411 return; 0412 0413 QStyleOptionProgressBar progressBarOption; 0414 progressBarOption.rect = rect.adjusted( 2, 2, -2, -2 ); 0415 progressBarOption.minimum = 0; 0416 progressBarOption.maximum = 100; 0417 progressBarOption.progress = m_weights[index] * 100.0; 0418 0419 QApplication::style()->drawControl(QStyle::CE_ProgressBar, &progressBarOption, painter); 0420 } 0421 0422 QList<qreal> 0423 Dynamic::PartBias::weights() const 0424 { 0425 return m_weights; 0426 } 0427 0428 Dynamic::TrackSet 0429 Dynamic::PartBias::matchingTracks( const Meta::TrackList& playlist, 0430 int contextCount, int finalCount, 0431 const Dynamic::TrackCollectionPtr &universe ) const 0432 { 0433 DEBUG_BLOCK; 0434 0435 // store the parameters in case we need to request additional matching tracks later 0436 m_playlist = playlist; 0437 m_contextCount = contextCount; 0438 m_finalCount = finalCount; 0439 m_universe = universe; 0440 0441 m_tracks = Dynamic::TrackSet(); 0442 m_matchingTracks.resize( m_biases.length() ); 0443 0444 // get the matching tracks from all sub-biases 0445 for( int i = 0; i < m_biases.length(); ++i ) 0446 m_matchingTracks[i] = m_biases[i]->matchingTracks( playlist, contextCount, finalCount, universe ); 0447 updateResults(); 0448 0449 return m_tracks; 0450 } 0451 0452 void 0453 Dynamic::PartBias::updateResults() const 0454 { 0455 DEBUG_BLOCK; 0456 0457 // -- first check if we have valid tracks from all sub-biases 0458 foreach( const Dynamic::TrackSet &tracks, m_matchingTracks ) 0459 if( tracks.isOutstanding() ) 0460 return; 0461 0462 // -- determine the current matching 0463 MatchState state( this, m_playlist, m_contextCount, m_finalCount ); 0464 0465 debug()<<"compute matching tracks for"<<m_finalCount<<"pc"<<m_playlist.count()<<"context:"<<m_contextCount; 0466 0467 // -- add all the tracks from one bias that has not fulfilled their capacity 0468 // biases still missing more tracks are picked more often 0469 // this prevents the problem that biases with only a few tracks always add their tracks 0470 // last 0471 int missingCapacity = 0; 0472 for( int source = 0; source < state.m_sourceCount; source++ ) 0473 { 0474 if( state.m_sourceFlow[source] < state.m_sourceCapacity[source] && 0475 m_matchingTracks[source].trackCount() > 0 ) 0476 missingCapacity += state.m_sourceCapacity[source] - state.m_sourceFlow[source]; 0477 } 0478 0479 m_tracks = Dynamic::TrackSet( m_universe, false ); 0480 0481 // if we have some biases under-represented 0482 if( missingCapacity > 0 ) 0483 { 0484 int random = QRandomGenerator::global()->generate() % missingCapacity; 0485 for( int source = 0; source < state.m_sourceCount; source++ ) 0486 { 0487 // debug() << "PartBias::matchingTracks: biase"<<m_biases[source]->toString()<<"matches"<<state.m_sourceFlow[source]<<"out of"<<state.m_sourceCapacity[source]<<"tracks."; 0488 if( state.m_sourceFlow[source] < state.m_sourceCapacity[source] && 0489 m_matchingTracks[source].trackCount() > 0 ) 0490 random -= state.m_sourceCapacity[source] - state.m_sourceFlow[source]; 0491 if( random < 0 ) 0492 { 0493 m_tracks.unite( m_matchingTracks[source] ); 0494 break; 0495 } 0496 } 0497 } 0498 0499 // else pick a random one. 0500 } 0501 0502 void 0503 Dynamic::PartBias::resultReceived( const Dynamic::TrackSet &tracks ) 0504 { 0505 int index = m_biases.indexOf(Dynamic::BiasPtr(qobject_cast<Dynamic::AbstractBias*>(sender()))); 0506 if( index < 0 ) { 0507 warning() << "Got results from a bias that I don't have."; 0508 return; 0509 } 0510 if( !m_tracks.isOutstanding() ) { 0511 warning() << "currently in resultReceived but we already have a solution"; 0512 return; 0513 } 0514 0515 m_matchingTracks[index] = tracks; 0516 updateResults(); 0517 0518 if( !m_tracks.isOutstanding() ) 0519 Q_EMIT resultReady( m_tracks ); 0520 } 0521 0522 bool 0523 Dynamic::PartBias::trackMatches( int position, 0524 const Meta::TrackList& playlist, 0525 int contextCount ) const 0526 { 0527 MatchState state( this, playlist, contextCount, playlist.count() ); 0528 0529 return ( state.m_drainFlow[position - contextCount] >= 0 ); 0530 } 0531 0532 void 0533 Dynamic::PartBias::appendBias( const Dynamic::BiasPtr &bias ) 0534 { 0535 DEBUG_BLOCK; 0536 m_weights.append( qreal(0.0) ); 0537 changeBiasWeight( 0, m_weights.at(0) ); // fix the weights to 1.0 again. 0538 AndBias::appendBias( bias ); 0539 } 0540 0541 void 0542 Dynamic::PartBias::moveBias( int from, int to ) 0543 { 0544 DEBUG_BLOCK; 0545 m_weights.insert( to, m_weights.takeAt( from ) ); 0546 AndBias::moveBias( from, to ); 0547 } 0548 0549 void 0550 Dynamic::PartBias::changeBiasWeight( int biasNum, qreal value ) 0551 { 0552 DEBUG_BLOCK; 0553 Q_ASSERT( biasNum >= 0 && biasNum < m_weights.count() ); 0554 0555 // the weights should sum up to 1.0 0556 0557 // -- only one weight. that is easy 0558 if( m_weights.count() == 1 ) 0559 { 0560 if( m_weights.at(0) != 1.0 ) 0561 { 0562 m_weights[0] = 1.0; 0563 Q_EMIT weightsChanged(); 0564 } 0565 return; 0566 } 0567 0568 // -- more than one. we have to modify the remaining. 0569 m_weights[biasNum] = qBound( qreal( 0.0 ), value, qreal( 1.0 ) ); 0570 0571 // - sum up all the weights 0572 qreal sum = 0.0; 0573 foreach( qreal v, m_weights ) 0574 sum += v; 0575 0576 // -- we are always using the first value to balance it out if possible 0577 if( biasNum != 0 ) 0578 { 0579 qreal oldV = m_weights.at(0); 0580 qreal newV = qBound<qreal>( qreal( 0.0 ), 1.0 - (sum - oldV), qreal( 1.0 ) ); 0581 m_weights[0] = newV; 0582 0583 sum = sum - oldV + newV; 0584 } 0585 0586 // modify all the remaining value 0587 0588 if( sum != 1.0 ) 0589 { 0590 if( sum - m_weights.at(biasNum) == 0.0 ) 0591 { 0592 // in this case the entry has all the weight. 0593 // distribute the remainder to the other weights 0594 for( int i = 0; i < m_weights.count(); i++ ) 0595 if( i != biasNum ) 0596 m_weights[i] = sum / (m_weights.count() - 1); 0597 0598 } 0599 else 0600 { 0601 // in this case we have some remaining weights. use a factor 0602 qreal factor = (1.0 - m_weights.at(biasNum)) / (sum - m_weights.at(biasNum)); 0603 for( int i = 0; i < m_weights.count(); i++ ) 0604 if( i != biasNum ) 0605 m_weights[i] *= factor; 0606 } 0607 } 0608 0609 for( int i = 0; i < m_weights.count(); i++ ) 0610 debug() << "Weight"<<i<<":"<<m_weights[i]; 0611 0612 Q_EMIT weightsChanged(); 0613 Q_EMIT changed( BiasPtr( this ) ); 0614 } 0615 0616 void 0617 Dynamic::PartBias::biasReplaced( const Dynamic::BiasPtr &oldBias, const Dynamic::BiasPtr &newBias ) 0618 { 0619 DEBUG_BLOCK; 0620 int index = m_biases.indexOf( oldBias ); 0621 if( !newBias ) 0622 { 0623 m_weights.takeAt( index ); 0624 if( !m_weights.isEmpty() ) 0625 changeBiasWeight( 0, m_weights.at(0) ); // fix the weights to 1.0 again. 0626 } 0627 0628 AndBias::biasReplaced( oldBias, newBias ); 0629 } 0630 0631