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