/*
* Copyright 2008-2013, ETH Zürich, Samuel Welten, Michael Kuhn, Tobias Langner,
* Sandro Affentranger, Lukas Bossard, Michael Grob, Rahul Jain,
* Dominic Langenegger, Sonia Mayor Alonso, Roger Odermatt, Tobias Schlueter,
* Yannick Stucki, Sebastian Wendland, Samuel Zehnder, Samuel Zihlmann,
* Samuel Zweifel
*
* This file is part of Jukefox.
*
* Jukefox is free software: you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or any later version. Jukefox is
* distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* Jukefox. If not, see <http://www.gnu.org/licenses/>.
*/
package ch.ethz.dcg.jukefox.playmode.smartshuffle.agents;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import ch.ethz.dcg.jukefox.commons.DataUnavailableException;
import ch.ethz.dcg.jukefox.commons.DataWriteException;
import ch.ethz.dcg.jukefox.commons.utils.Log;
import ch.ethz.dcg.jukefox.data.db.IDbDataPortal;
import ch.ethz.dcg.jukefox.model.providers.SongProvider;
import ch.ethz.dcg.jukefox.model.providers.StatisticsProvider;
import ch.ethz.dcg.jukefox.playmode.smartshuffle.agents.AbstractRecentAgent.TimeFilter;
public class AgentManager {
private final static String TAG = AgentManager.class.getSimpleName();
/**
* The general type of an agent. There are subtypes depending on the agent type.
*/
public enum AgentType {
Random,
Top,
Suggested,
Repetition
}
/**
* The minimum weight an agent can have. ∈ [0, {@value #MAX_AGENT_WEIGHT})
*/
public final static double MIN_AGENT_WEIGHT = 0.01d;
/**
* The maximum weight an agent can have. ∈ ({@value #MIN_AGENT_WEIGHT}, 1]
*/
public final static double MAX_AGENT_WEIGHT = 1.0d;
/**
* The factor which will be used to adjust the weights in {@link #adjustAgentWeights(Map, float)}.
*/
private final static double WEIGHT_CHANGE_FACTOR = 0.1d;
/**
* Agents with their weights. Weights are ∈ [{@value #MIN_AGENT_WEIGHT}, {@value #MAX_AGENT_WEIGHT}].
*/
private Map<IAgent, Double> agentWeights;
private IDbDataPortal dbDataPortal;
/**
* The singleton instance
*/
private static AgentManager instance = null;
private boolean initialized = false;
private Map<IAgent, Double> adjustmentBoost = new HashMap<IAgent, Double>();
private AgentManager() {
}
/**
* Initializes the AgentManager singleton instance. If this method is called twice it simply does return the
* singleton-instance and does no reinitialization.
*
* @param dbDataPortal
* @param songProvider
* @param statisticsProvider
* @return The singleton instance
*/
public static AgentManager initialize(IDbDataPortal dbDataPortal, SongProvider songProvider,
StatisticsProvider statisticsProvider) {
AgentManager self = AgentManager.getInstance();
synchronized (self) {
if (self.initialized) {
return self;
}
self.initialized = true;
}
self.dbDataPortal = dbDataPortal;
// Register the agents
self.agentWeights = new HashMap<IAgent, Double>();
self.registerAgent(new RandomAgent(songProvider), 0.5d);
self.registerAgent(new TopArtistAgent(songProvider, statisticsProvider, TimeFilter.HOUR_OF_THE_DAY), 0.5d);
//self.registerAgent(new TopArtistAgent(songProvider, statisticsProvider, TimeFilter.DAY_OF_THE_WEEK), 0.5d);
self.registerAgent(new TopArtistAgent(songProvider, statisticsProvider, TimeFilter.RECENTLY), 0.5d);
self.registerAgent(new TopArtistAgent(songProvider, statisticsProvider, TimeFilter.NONE), 0.5d);
self.registerAgent(new SuggestedAgent(statisticsProvider, TimeFilter.HOUR_OF_THE_DAY), 0.5d);
//self.registerAgent(new SuggestedAgent(statisticsProvider, TimeFilter.DAY_OF_THE_WEEK), 0.5d);
self.registerAgent(new SuggestedAgent(statisticsProvider, TimeFilter.RECENTLY), 0.5d);
self.registerAgent(new SuggestedAgent(statisticsProvider, TimeFilter.NONE), 0.5d);
self.registerAgent(new SongRepetitionAgent(statisticsProvider), 0.5d);
self.registerAgent(new ArtistRepetitionAgent(statisticsProvider), 0.5d);
// Normalize their weights
self.normalizeAgentWeights();
return self;
}
public static AgentManager getInstance() {
if (instance == null) {
synchronized (AgentManager.class) {
if (instance == null) {
instance = new AgentManager();
}
}
}
return instance;
}
/**
* Returns the registered agents.
*
* @return The agents
*/
public Set<IAgent> getAgents() {
return agentWeights.keySet();
}
/**
* Adds the given agent to the list and loads its weight from the database. If no weight is in the database, we set
* it to defaultWeight.
*
* @param agent
* The agent
* @param defaultWeight
* The weight of this agent if it can not be found in the database
*/
protected void registerAgent(IAgent agent, double defaultWeight) {
double weight;
try {
weight = dbDataPortal.getKeyValueDouble("agents.weight", agent.getIdentifier());
} catch (DataUnavailableException e) {
weight = defaultWeight;
}
agentWeights.put(agent, weight);
}
/**
* @see #normalizeWeights(Map)
*/
private void normalizeAgentWeights() {
normalizeWeights(agentWeights);
}
/**
* Normalizes the weights in the given map, so that they are in [{@value #MIN_AGENT_WEIGHT},
* {@value #MAX_AGENT_WEIGHT}] and sum up to 1.
*
* @param weights
* The weights
*/
public static <T extends Object> void normalizeWeights(Map<T, Double> weights) {
double weightSum = 0.0d;
for (double weight : weights.values()) {
weight = Math.max(weight, MIN_AGENT_WEIGHT);
weight = Math.min(weight, MAX_AGENT_WEIGHT);
weightSum += weight;
}
for (Map.Entry<T, Double> entry : weights.entrySet()) {
double newWeight = entry.getValue() * 1 / weightSum;
newWeight = Math.max(newWeight, MIN_AGENT_WEIGHT);
newWeight = Math.min(newWeight, MAX_AGENT_WEIGHT);
weights.put(entry.getKey(), newWeight);
}
}
/**
* Returns true, if we should adjust the agents weight automatically by their voting correctness.
*
* @return
*/
public boolean isAutoAdjustAgentWeights() {
try {
return dbDataPortal.getKeyValueInt("agents", "auto_adjust_weight") != 0;
} catch (DataUnavailableException e) {
return true;
}
}
/**
* Setter for if we should adjust the agents weight automatically by their voting correctness.
*
* @param doIt
* If we should do it
*/
public void setAutoAdjustAgentWeights(boolean doIt) {
try {
dbDataPortal.setKeyValue("agents", "auto_adjust_weight", doIt ? 1 : 0);
} catch (DataWriteException e) {
Log.w(TAG, e);
}
}
/**
* Returns the weight of the given agent.
*
* @param agent
* The agent
* @return The weight ∈ [{@value #MIN_AGENT_WEIGHT}, {@value #MAX_AGENT_WEIGHT}]
*/
public double getAgentWeight(IAgent agent) {
return agentWeights.get(agent);
}
/**
* Returns the weight of all the agents.
*
* @return The weight ∈ [{@value #MIN_AGENT_WEIGHT}, {@value #MAX_AGENT_WEIGHT}]
*/
public Map<IAgent, Double> getAgentWeights() {
return agentWeights;
}
/**
* Adjusts out of bounds weights and sets it for the agent afterwards.
*
* @param agent
* The agent
* @param weight
* The new weight ∈ [{@value #MIN_AGENT_WEIGHT}, {@value #MAX_AGENT_WEIGHT}]
*/
private void setAgentWeight(IAgent agent, double weight) {
weight = Math.max(MIN_AGENT_WEIGHT, weight);
weight = Math.min(MAX_AGENT_WEIGHT, weight);
agentWeights.put(agent, weight);
}
/**
* Sets the weight of the agents and saves them.<br/>
* Attention: In most cases you want to call {@link #adjustAgentWeights(Map, float)}!
*
* @param agentWeights
* The agent-weight map
*/
public void setAgentWeights(Map<IAgent, Double> agentWeights) {
// Set the weights
for (Map.Entry<IAgent, Double> entry : agentWeights.entrySet()) {
setAgentWeight(entry.getKey(), entry.getValue());
}
// Normalize the weights
normalizeAgentWeights();
// Save the weights
saveAgentWeights();
}
/**
* Sets the weights of the agents in the given agent group. The groupWeight is distributed to the agents by the same
* fraction the old weights are distributed among them.
*
* @param agents
* The agents in this group
* @param groupWeight
* The new weight of the whole group
*/
private void setAgentsGroupWeight(List<IAgent> agents, double groupWeight) {
double agentsWeightSum = 0.0d;
for (IAgent agent : agents) {
agentsWeightSum += getAgentWeight(agent);
}
for (IAgent agent : agents) {
double weightFraction = getAgentWeight(agent) / agentsWeightSum;
setAgentWeight(agent, groupWeight * weightFraction);
}
}
/**
* Sets the weights of the agents in the given agent groups.
*
* @param groupWeights
* The weights of the groups
* @see #setAgentsGroupWeight(List, double)
*/
public void setAgentsGroupWeights(Map<List<IAgent>, Double> groupWeights) {
// Set the weights
for (Map.Entry<List<IAgent>, Double> entry : groupWeights.entrySet()) {
setAgentsGroupWeight(entry.getKey(), entry.getValue());
}
// Normalize the weights
normalizeAgentWeights();
// Save the weights
saveAgentWeights();
}
/**
* Stores the weights of the agents to the database.<br/>
* Please call {@link #normalizeAgentWeights()} first.
*/
private void saveAgentWeights() {
for (Map.Entry<IAgent, Double> entry : agentWeights.entrySet()) {
try {
dbDataPortal.setKeyValue("agents.weight", entry.getKey().getIdentifier(), entry.getValue());
} catch (DataWriteException e) {
Log.w(TAG, e);
}
}
}
/**
* Increases or decreases the weight of the agents. The base for the adjustment is agentVote*songRating. Therefore,
* if an agents vote is similar to the actual rating, its weight is increased and if it is very different, the
* weight is decreased. Moreover, agents with strong votes (where |vote| ≈ 1) get more adjusted than these
* with moderate votes (A vote of 0 results in no weight adjustment at all).<br/>
* We also are choosing the adjustments to get bigger if an agent votes wrong multiple times in a row (see
* {@link #getAdjustment(IAgent, double)}).
*
* <pre>
* w<sub>new</sub> = (1 - {@value #WEIGHT_CHANGE_FACTOR}) * w<sub>old</sub> + {@value #WEIGHT_CHANGE_FACTOR} * adjustment
* </pre>
*
* To see, how <code>adjustment</code> is determined, see
*
* @param agentVotes
* The agents votes
* @param songRating
* The song rating
* @see #getAdjustment(IAgent, double)
*/
public void adjustAgentWeights(Map<IAgent, Float> agentVotes, float songRating) {
if (agentVotes == null) {
return;
}
if (!isAutoAdjustAgentWeights()) {
return;
}
// Adjust the weights
Map<IAgent, Double> oldWeights = new HashMap<IAgent, Double>(agentWeights);
for (Map.Entry<IAgent, Float> agentVote : agentVotes.entrySet()) {
final IAgent agent = agentVote.getKey();
double ratingVoteProduct = (double) songRating * agentVote.getValue(); // in [-1, 1]
double adjustment = getAdjustment(agent, ratingVoteProduct);
double oldWeight = getAgentWeight(agent);
oldWeights.put(agent, oldWeight);
double newWeight = (1 - WEIGHT_CHANGE_FACTOR) * oldWeight + WEIGHT_CHANGE_FACTOR * adjustment;
setAgentWeight(agent, newWeight);
}
// Normalize the weights
normalizeAgentWeights();
String diffMsg = "Weight diffs:\n";
for (Map.Entry<IAgent, Double> entry : agentWeights.entrySet()) {
IAgent agent = entry.getKey();
diffMsg += String.format(" %s: %.2f\n", agent.getIdentifier(),
entry.getValue() - oldWeights.get(agent));
}
Log.d(TAG, diffMsg);
// Store them
saveAgentWeights();
}
/**
* Returns the adjustment for the given agent-ratingVoteProduct combination. The returned value is
*
* <pre>
* adjustment = weight(agent) + boost<sub>i</sub>(agent) * ratingVoteProduct
* </pre>
*
* Boost is calculated as
*
* <pre>
* ratingVoteProduct >= 0 => boost<sub>i</sub>(agent) = boost<sub>i+1</sub>(agent) = 1<br/>
* ratingVoteProduct < 0 => boost<sub>i+1</sub>(agent) = boost<sub>i</sub>(agent) + |ratingVoteProduct|
* </pre>
*
* We use boost to penalize repeatly negative {ratingVoteProduct}s.
*
* @param agent
* The agent
* @param ratingVoteProduct
* The rating of the song times the vote of the agent.
* @return The adjustment which should be used
* @see #adjustAgentWeights(Map, float)
*/
private double getAdjustment(final IAgent agent, double ratingVoteProduct) {
Double boost = adjustmentBoost.get(agent);
if ((ratingVoteProduct >= 0) || (boost == null)) {
boost = 1.0d;
}
double adjustment = agentWeights.get(agent) + boost * ratingVoteProduct;
if (ratingVoteProduct < 0) {
boost += -1 * ratingVoteProduct; // equals boost += abs(ratingVoteProduct)
}
adjustmentBoost.put(agent, boost);
return adjustment;
}
}