Beispiel #1
0
        /// <summary>
        /// Generates a model with the specified number of recipes and returns the recipe IDs in the optimal order.
        /// </summary>
        /// <param name="recipes">Number of recipes to generate</param>
        /// <param name="scale">Scale indicating importance of optimal ingredient usage vs. user trend usage. 1 indicates ignore user trends, return most efficient set of recipes. 5 indicates ignore pantry and generate recipes user is most likely to rate high.</param>
        /// <returns>An array up to size "recipes" containing recipes from DBSnapshot.</returns>
        public Model Generate(int recipes, byte scale)
        {
            if (recipes > MAX_SUGGESTIONS)
            {
                throw new ArgumentException("Modeler can only generate " + MAX_SUGGESTIONS.ToString() + " recipes at a time.");
            }

            var          temperature         = 10000.0;
            double       deltaScore          = 0;
            const double absoluteTemperature = 0.00001;

            totals = new Dictionary <IngredientNode, IngredientUsage>(IngredientNode.NextKey);

            var currentSet = new RecipeNode[recipes]; //current set of recipes
            var nextSet    = new RecipeNode[recipes]; //set to compare with current

            InitSet(currentSet);                      //Initialize with n random recipes
            var score = GetScore(currentSet, scale);  //Check initial score

            var timer = new Stopwatch();

            timer.Start();

            while (temperature > absoluteTemperature)
            {
                nextSet    = GetNextSet(currentSet); //Swap out a random recipe with another one from the available pool
                deltaScore = GetScore(nextSet, scale) - score;

                //if the new set has a smaller score (good thing)
                //or if the new set has a higher score but satisfies Boltzman condition then accept the set
                if ((deltaScore < 0) || (score > 0 && Math.Exp(-deltaScore / temperature) > random.NextDouble()))
                {
                    nextSet.CopyTo(currentSet, 0);
                    score += deltaScore;
                }

                //cool down the temperature
                temperature *= COOLING_RATE;
            }

            timer.Stop();
            Log.InfoFormat("Generating set of {0} recipes took {1}ms.", recipes, timer.ElapsedMilliseconds);

            return(new Model(currentSet, profile.Pantry, score));
        }
Beispiel #2
0
        /// <summary>
        /// Swap out a random recipe with another one from the available pool
        /// </summary>
        RecipeNode[] GetNextSet(RecipeNode[] currentSet)
        {
            var        rndIndex       = random.Next(currentSet.Length);
            var        existingRecipe = currentSet[rndIndex];
            RecipeNode newRecipe;

            var timeout = 0;

            while (true)
            {
                if (++timeout > 100)                      //We've tried 100 times to replace a recipe in this set, but cannot find anything that isn't already in this set.
                {
                    throw new ImpossibleQueryException(); //TODO: If this is the only set of n which match that query, we've solved it - just return this set as final!
                }

                newRecipe = Fish();
                if (newRecipe == existingRecipe)
                {
                    continue;
                }

                var fFound = false;
                for (var i = 0; i < currentSet.Length; i++)
                {
                    if (newRecipe == currentSet[i])
                    {
                        fFound = true;
                        break;
                    }
                }

                if (!fFound)
                {
                    break;
                }
            }

            var retSet = new RecipeNode[currentSet.Length];

            currentSet.CopyTo(retSet, 0);
            retSet[rndIndex] = newRecipe;

            return(retSet);
        }
 public Model(RecipeNode[] recipes, PantryItem[] pantry, double score)
 {
     this.recipeids = recipes.Select(r => r.RecipeId).ToArray();
     this.pantry = pantry;
     this.score = score;
 }
        /// <summary>
        /// Judges a set of recipes based on a scale and its efficiency with regards to the current pantry.  The lower the score, the better.
        /// </summary>
        double GetScore(RecipeNode[] currentSet, byte scale)
        {
            double wasted = 0; //Add 1.0 for ingredients that don't exist in pantry, add percentage of leftover otherwise
             float avgRating = 0; //Average rating for all recipes in the set (0-4)
             float tagPoints = 0; //Point for each tag that's one of our favorites
             float tagTotal = 0; //Total number of tags in all recipes
             float ingPoints = 0; //Point for each ing that's one of our favorites
             float ingTotal = 0; //Total number of ingrediets in all recipes

             for (var i = 0; i < currentSet.Length; i++)
             {
            var recipe = currentSet[i];
            var ingredients = (IngredientUsage[]) recipe.Ingredients;

            //Add points for any favorite tags this recipe uses

            tagTotal += recipe.Tags.Length;
            ingTotal += ingredients.Length;

            for (var t = 0; t < recipe.Tags.Length; t++) //TODO: Use bitmasks for storing recipe tags and fav tags, then count bits
            {
               if (favTags[t])
               {
                  tagPoints++;
               }
            }

            byte realRating; //Real rating is the user's rating, else the public rating, else 3.
            if (!ratings.TryGetValue(recipe, out realRating))
            {
               realRating = (recipe.Rating == 0) ? (byte) 3 : recipe.Rating; //if recipe has no ratings, use average rating of 3.
            }
            avgRating += (realRating - 1);

            for (var j = 0; j < ingredients.Length; j++)
            {
               var ingredient = ingredients[j];

               //Add points for any favorite ingredients this recipe uses
               var ingKey = ingredient.Ingredient.Key;

               for (var k = 0; k < favIngs.Length; k++) //For loop is actually faster than 5 "if" statements
               {
                  if (favIngs[k] == ingKey)
                  {
                     ingPoints++;
                     break;
                  }
               }

               IngredientUsage curUsage;
               var fContains = totals.TryGetValue(ingredient.Ingredient, out curUsage);
               if (!fContains)
               {
                  curUsage = new IngredientUsage();
                  curUsage.Amt = ingredient.Amt;
                  totals.Add(ingredient.Ingredient, curUsage);
               }
               else
               {
                  curUsage.Amt += ingredient.Amt;
               }
            }
             }

             if (profile.Pantry != null) //If profile has a pantry, figure out how much of it is wasted
             {
            //For each pantry ingredient that we're not using, punish the score by MISSING_ING_PUNISH amount.
            var pEnum = pantryAmounts.GetEnumerator();
            while (pEnum.MoveNext())
            {
               if (!totals.ContainsKey(pEnum.Current.Key))
               {
                  wasted += MISSING_ING_PUNISH;
               }
            }

            var e = totals.GetEnumerator();
            while (e.MoveNext())
            {
               var curKey = e.Current.Key;

               float? have;
               if (pantryAmounts.TryGetValue(curKey, out have)) //We have this in our pantry
               {
                  if (!have.HasValue) //We have this in our pantry, but no amount is specified - So we "act" like we have whatever we need
                  {
                     continue;
                  }

                  if (!e.Current.Value.Amt.HasValue) //This recipe doesn't specify an amount - So we "act" like we use half of what we have
                  {
                     wasted += EMPTY_RECIPE_AMOUNT;
                     continue;
                  }

                  var need = e.Current.Value.Amt.Value;
                  var ratio = 1 - ((have.Value - need)/have.Value); //Percentage of how much you're using of what you have
                  if (ratio > 1) //If you need more than you have, add the excess ratio to the waste but don't go over the punishment for not having the ingredient at all
                  {
                     wasted += Math.Min(ratio, NEW_ING_PUNISH);
                  }
                  else
                  {
                     wasted += (1 - ratio);
                  }
               }
               else
               {
                  wasted += NEW_ING_PUNISH; //For each ingredient this meal set needs that we don't have, increment by NEW_ING_PUNISH
               }
            }
             }

             double worstScore, trendScore, efficiencyScore;

             if (profile.Pantry == null) //No pantry, efficiency is defined by the overlap of ingredients across recipes
             {
            efficiencyScore = totals.Keys.Count/ingTotal;
             }
             else //Efficiency is defined by how efficient the pantry ingredients are utilized
             {
            worstScore = ((totals.Keys.Count*NEW_ING_PUNISH) + (profile.Pantry.Length*MISSING_ING_PUNISH)); //Worst possible efficiency score
            efficiencyScore = (wasted/worstScore);
             }

             avgRating /= currentSet.Length;
             trendScore = 1 - ((((avgRating/4)*4) + (tagPoints/tagTotal) + (ingPoints/ingTotal))/6);

             totals.Clear();

             if (scale == 1)
            return efficiencyScore;
             else if (scale == 2)
            return (efficiencyScore + efficiencyScore + trendScore)/3;
             else if (scale == 3)
            return (efficiencyScore + trendScore)/2;
             else if (scale == 4)
            return (efficiencyScore + trendScore + trendScore)/3;
             else if (scale == 5)
            return trendScore;

             return 0;
        }
        /// <summary>
        /// Initializes currentSet with random recipes from the available recipe pool.
        /// </summary>
        void InitSet(RecipeNode[] currentSet)
        {
            var inUse = new List<Guid>(currentSet.Length);

             for (var i = 0; i < currentSet.Length; i++)
             {
            RecipeNode g;
            var timeout = 0;
            do
            {
               g = Fish();

               if (++timeout > 100) //Ok we've tried 100 times to find a recipe not already in this set, there just isn't enough data to work with for this query
               {
                  throw new ImpossibleQueryException(); //TODO: Maybe we can lower the demanded meals and return what we do have
               }
            } while (inUse.Contains(g.RecipeId));

            inUse.Add(g.RecipeId);
            currentSet[i] = g;
             }
        }
        /// <summary>
        /// Generates a model with the specified number of recipes and returns the recipe IDs in the optimal order.
        /// </summary>
        /// <param name="recipes">Number of recipes to generate</param>
        /// <param name="scale">Scale indicating importance of optimal ingredient usage vs. user trend usage. 1 indicates ignore user trends, return most efficient set of recipes. 5 indicates ignore pantry and generate recipes user is most likely to rate high.</param>
        /// <returns>An array up to size "recipes" containing recipes from DBSnapshot.</returns>
        public Model Generate(int recipes, byte scale)
        {
            if (recipes > MAX_SUGGESTIONS)
             {
            throw new ArgumentException("Modeler can only generate " + MAX_SUGGESTIONS.ToString() + " recipes at a time.");
             }

             var temperature = 10000.0;
             double deltaScore = 0;
             const double absoluteTemperature = 0.00001;

             totals = new Dictionary<IngredientNode, IngredientUsage>(IngredientNode.NextKey);

             var currentSet = new RecipeNode[recipes]; //current set of recipes
             var nextSet = new RecipeNode[recipes]; //set to compare with current
             InitSet(currentSet); //Initialize with n random recipes
             var score = GetScore(currentSet, scale); //Check initial score

             var timer = new Stopwatch();
             timer.Start();

             while (temperature > absoluteTemperature)
             {
            nextSet = GetNextSet(currentSet); //Swap out a random recipe with another one from the available pool
            deltaScore = GetScore(nextSet, scale) - score;

            //if the new set has a smaller score (good thing)
            //or if the new set has a higher score but satisfies Boltzman condition then accept the set
            if ((deltaScore < 0) || (score > 0 && Math.Exp(-deltaScore/temperature) > random.NextDouble()))
            {
               nextSet.CopyTo(currentSet, 0);
               score += deltaScore;
            }

            //cool down the temperature
            temperature *= COOLING_RATE;
             }

             timer.Stop();
             Log.InfoFormat("Generating set of {0} recipes took {1}ms.", recipes, timer.ElapsedMilliseconds);

             return new Model(currentSet, profile.Pantry, score);
        }
        /// <summary>
        /// Swap out a random recipe with another one from the available pool
        /// </summary>
        RecipeNode[] GetNextSet(RecipeNode[] currentSet)
        {
            var rndIndex = random.Next(currentSet.Length);
             var existingRecipe = currentSet[rndIndex];
             RecipeNode newRecipe;

             var timeout = 0;
             while (true)
             {
            if (++timeout > 100) //We've tried 100 times to replace a recipe in this set, but cannot find anything that isn't already in this set.
            {
               throw new ImpossibleQueryException(); //TODO: If this is the only set of n which match that query, we've solved it - just return this set as final!
            }

            newRecipe = Fish();
            if (newRecipe == existingRecipe)
            {
               continue;
            }

            var fFound = false;
            for (var i = 0; i < currentSet.Length; i++)
            {
               if (newRecipe == currentSet[i])
               {
                  fFound = true;
                  break;
               }
            }

            if (!fFound)
            {
               break;
            }
             }

             var retSet = new RecipeNode[currentSet.Length];
             currentSet.CopyTo(retSet, 0);
             retSet[rndIndex] = newRecipe;

             return retSet;
        }
Beispiel #8
0
 public Model(RecipeNode[] recipes, PantryItem[] pantry, double score)
 {
     this._recipeids = recipes.Select(r => { return r.RecipeId; }).ToArray();
      this._pantry = pantry;
      this._score = score;
 }
        /// <summary>
        /// Judges a set of recipes based on a scale and its efficiency with regards to the current pantry.  The lower the score, the better.
        /// </summary>
        public double GetScore(RecipeNode[] currentSet, byte scale)
        {
            double wasted = 0; // Add 1.0 for ingredients that don't exist in pantry, add percentage of leftover otherwise
            float avgRating = 0; // Average rating for all recipes in the set (0-4)
            float tagPoints = 0; // Point for each tag that's one of our favorites
            float tagTotal = 0; // Total number of tags in all recipes
            float ingPoints = 0; // Point for each ing that's one of our favorites
            float ingTotal = 0; // Total number of ingrediets in all recipes

            for (var i = 0; i < currentSet.Length; i++)
            {
                var recipe = currentSet[i];
                var ingredients = (IngredientUsage[])recipe.Ingredients;

                // Add points for any favorite tags this recipe uses
                tagTotal += recipe.Tags.Length;
                ingTotal += ingredients.Length;

                for (var t = 0; t < recipe.Tags.Length; t++)
                {
                    if (this.favTags[t])
                    {
                        tagPoints++;
                    }
                }

                byte realRating; // Real rating is the user's rating, else the public rating, else 3.
                if (!this.ratings.TryGetValue(recipe, out realRating))
                {
                    realRating = (recipe.Rating == 0) ? (byte)3 : recipe.Rating; // if recipe has no ratings, use average rating of 3.
                }
                avgRating += realRating - 1;

                foreach (var ingredient in ingredients)
                {
                    // Add points for any favorite ingredients this recipe uses
                    var ingKey = ingredient.Ingredient.Key;

                    if (this.favIngs.Any(t => t == ingKey))
                    {
                        ingPoints++;
                    }

                    IngredientUsage curUsage;
                    var fContains = this.totals.TryGetValue(ingredient.Ingredient, out curUsage);
                    if (!fContains)
                    {
                        curUsage = new IngredientUsage();
                        curUsage.Amt = ingredient.Amt;
                        this.totals.Add(ingredient.Ingredient, curUsage);
                    }
                    else
                    {
                        curUsage.Amt += ingredient.Amt;
                    }
                }
            }

            // If profile has a pantry, figure out how much of it is wasted
            if (this.profile.Pantry != null)
            {
                // For each pantry ingredient that we're not using, punish the score by MISSING_ING_PUNISH amount.
                var pEnum = this.pantryAmounts.GetEnumerator();
                while (pEnum.MoveNext())
                {
                    if (!this.totals.ContainsKey(pEnum.Current.Key))
                    {
                        wasted += MissingIngPunish;
                    }
                }

                var e = this.totals.GetEnumerator();
                while (e.MoveNext())
                {
                    var curKey = e.Current.Key;

                    float? have;

                    // We have this in our pantry
                    if (this.pantryAmounts.TryGetValue(curKey, out have))
                    {
                        // We have this in our pantry, but no amount is specified - So we "act" like we have whatever we need
                        if (!have.HasValue)
                        {
                            continue;
                        }

                        // This recipe doesn't specify an amount - So we "act" like we use half of what we have
                        if (!e.Current.Value.Amt.HasValue)
                        {
                            wasted += EmptyRecipeAmount;
                            continue;
                        }

                        var need = e.Current.Value.Amt.Value;
                        var ratio = 1 - ((have.Value - need) / have.Value); // Percentage of how much you're using of what you have
                        // If you need more than you have, add the excess ratio to the waste but don't go over the punishment for not having the ingredient at all
                        if (ratio > 1)
                        {
                            wasted += Math.Min(ratio, NewIngPunish);
                        }
                        else
                        {
                            wasted += 1 - ratio;
                        }
                    }
                    else
                    {
                        wasted += NewIngPunish; // For each ingredient this meal set needs that we don't have, increment by NEW_ING_PUNISH
                    }
                }
            }

            double efficiencyScore;

            // No pantry, efficiency is defined by the overlap of ingredients across recipes
            if (this.profile.Pantry == null)
            {
                efficiencyScore = totals.Keys.Count / ingTotal;
            }
            else
            {
                // Efficiency is defined by how efficient the pantry ingredients are utilized
                double worstScore = (this.totals.Keys.Count * NewIngPunish) + (this.profile.Pantry.Length * MissingIngPunish);
                efficiencyScore = wasted / worstScore;
            }

            avgRating /= currentSet.Length;
            double trendScore = 1 - ((((avgRating / 4) * 4) + (tagPoints / tagTotal) + (ingPoints / ingTotal)) / 6);

            this.totals.Clear();

            switch (scale)
            {
                case 1:
                    return efficiencyScore;
                case 2:
                    return (efficiencyScore + efficiencyScore + trendScore) / 3;
                case 3:
                    return (efficiencyScore + trendScore) / 2;
                case 4:
                    return (efficiencyScore + trendScore + trendScore) / 3;
                case 5:
                    return trendScore;
            }

            return 0;
        }
        /// <summary>
        /// Generates a model with the specified number of recipes and returns the recipe IDs in the optimal order.
        /// </summary>
        /// <param name="recipes">Number of recipes to generate</param>
        /// <param name="scale">Scale indicating importance of optimal ingredient usage vs. user trend usage. 1 indicates ignore user trends, return most efficient set of recipes. 5 indicates ignore pantry and generate recipes user is most likely to rate high.</param>
        /// <returns>An array up to size "recipes" containing recipes from DBSnapshot.</returns>
        public Model Generate(int recipes, byte scale)
        {
            if (recipes > MaxSuggestions)
            {
                throw new ArgumentException("Modeler can only generate " + MaxSuggestions + " recipes at a time.");
            }

            var temperature = 10000.0;
            const double AbsoluteTemperature = 0.00001;

            this.totals = new Dictionary<IngredientNode, IngredientUsage>(IngredientNode.NextKey);

            var currentSet = new RecipeNode[recipes]; // current set of recipes
            this.InitSet(currentSet); // Initialize with n random recipes
            var score = this.GetScore(currentSet, scale); // Check initial score

            var timer = new Stopwatch();
            timer.Start();

            while (temperature > AbsoluteTemperature)
            {
                var nextSet = this.GetNextSet(currentSet); // set to compare with current
                var deltaScore = this.GetScore(nextSet, scale) - score;

                // if the new set has a smaller score (good thing)
                // or if the new set has a higher score but satisfies Boltzman condition then accept the set
                if ((deltaScore < 0) || (score > 0 && Math.Exp(-deltaScore / temperature) > this.random.NextDouble()))
                {
                    nextSet.CopyTo(currentSet, 0);
                    score += deltaScore;
                }

                // cool down the temperature
                temperature *= CoolingRate;
            }

            timer.Stop();
            Log.InfoFormat("Generating set of {0} recipes took {1}ms.", recipes, timer.ElapsedMilliseconds);

            return new Model(currentSet, this.profile.Pantry, score);
        }
        /// <summary>
        /// Judges a set of recipes based on a scale and its efficiency with regards to the current pantry.  The lower the score, the better.
        /// </summary>
        private double GetScore(RecipeNode[] currentSet, byte scale)
        {
            double wasted = 0; // Add 1.0 for ingredients that don't exist in pantry, add percentage of leftover otherwise
            float averageRating = 0; // Average rating for all recipes in the set (0-4)
            float tagPoints = 0; // Point for each tag that's one of our favorites
            float tagTotal = 0; // Total number of tags in all recipes
            float ingredientPoints = 0; // Point for each ingredient that's one of our favorites
            float ingredientTotal = 0; // Total number of ingrediets in all recipes

            for (var i = 0; i < currentSet.Length; i++)
            {
                var recipe = currentSet[i];
                var ingredients = (IngredientUsage[])recipe.Ingredients;

                // Add points for any favorite tags this recipe uses
                tagTotal += recipe.Tags.Length;
                ingredientTotal += ingredients.Length;

                // TODO: Use bitmasks for storing recipe tags and fav tags, then count bits
                for (int tag = 0; tag < recipe.Tags.Length; tag++)
                {
                    if (this.favoriteTags[tag])
                    {
                        tagPoints++;
                    }
                }

                // Real rating is the user's rating, else the public rating, else 3.
                byte realRating;
                if (!this.ratings.TryGetValue(recipe, out realRating))
                {
                    // if recipe has no ratings, use average rating of 3.
                    realRating = (recipe.Rating == 0) ? (byte)3 : recipe.Rating;
                }

                averageRating += realRating - 1;

                for (var j = 0; j < ingredients.Length; j++)
                {
                    var ingredient = ingredients[j];

                    // Add points for any favorite ingredients this recipe uses
                    var ingredientKey = ingredient.Ingredient.Key;

                    for (var key = 0; key < this.favoriteIngredients.Length; key++)
                    {
                        if (this.favoriteIngredients[key] == ingredientKey)
                        {
                            ingredientPoints++;
                            break;
                        }
                    }

                    IngredientUsage currentUsage;
                    bool fContains = this.totals.TryGetValue(ingredient.Ingredient, out currentUsage);
                    if (!fContains)
                    {
                        currentUsage = new IngredientUsage();
                        currentUsage.Amount = ingredient.Amount;
                        this.totals.Add(ingredient.Ingredient, currentUsage);
                    }
                    else
                    {
                        currentUsage.Amount += ingredient.Amount;
                    }
                }
            }

            // If profile has a pantry, figure out how much of it is wasted
            if (this.profile.Pantry != null)
            {
                // For each pantry ingredient that we're not using, punish the score by MissingIngredientPunish amount.
                var pantryEnumerator = this.pantryAmounts.GetEnumerator();
                while (pantryEnumerator.MoveNext())
                {
                    if (!this.totals.ContainsKey(pantryEnumerator.Current.Key))
                    {
                        wasted += MissingIngredientPunish;
                    }
                }

                var enumerator = this.totals.GetEnumerator();
                while (enumerator.MoveNext())
                {
                    var currentKey = enumerator.Current.Key;

                    float? haveAmount;

                    // We have this in our pantry
                    if (this.pantryAmounts.TryGetValue(currentKey, out haveAmount))
                    {
                        // We have this in our pantry, but no amount is specified - So we "act" like we have whatever we need
                        if (!haveAmount.HasValue)
                        {
                            continue;
                        }

                        // This recipe doesn't specify an amount - So we "act" like we use half of what we have
                        if (!enumerator.Current.Value.Amount.HasValue)
                        {
                            wasted += EmptyRecipeAmount;
                            continue;
                        }

                        float needAmount = enumerator.Current.Value.Amount.Value;

                        // Percentage of how much you're using of what you have
                        float ratio = 1 - ((haveAmount.Value - needAmount) / haveAmount.Value);

                        // If you need more than you have, add the excess ratio to the waste but don't go over the punishment for not having the ingredient at all
                        if (ratio > 1)
                        {
                            wasted += Math.Min(ratio, NewIngredientPunish);
                        }
                        else
                        {
                            wasted += 1 - ratio;
                        }
                    }
                    else
                    {
                        // For each ingredient this meal set needs that we don't have, increment by NewIngredientPunish
                        wasted += NewIngredientPunish;
                    }
                }
            }

            double worstScore;
            double trendScore;
            double efficiencyScore;

            // No pantry, efficiency is defined by the overlap of ingredients across recipes
            if (this.profile.Pantry == null)
            {
                efficiencyScore = this.totals.Keys.Count / ingredientTotal;
            }
            else
            {
                // Efficiency is defined by how efficient the pantry ingredients are utilized
                worstScore =
                    this.totals.Keys.Count * NewIngredientPunish +
                    this.profile.Pantry.Length * MissingIngredientPunish; // Worst possible efficiency score
                efficiencyScore = wasted / worstScore;
            }

            averageRating /= currentSet.Length;
            trendScore = 1 - ((averageRating + tagPoints / tagTotal + ingredientPoints / ingredientTotal) / 6);

            this.totals.Clear();

            switch (scale)
            {
                case 1:
                    return efficiencyScore;
                case 2:
                    return (efficiencyScore + efficiencyScore + trendScore) / 3;
                case 3:
                    return (efficiencyScore + trendScore) / 2;
                case 4:
                    return (efficiencyScore + trendScore + trendScore) / 3;
                case 5:
                    return trendScore;
                default:
                    return 0;
            }
        }
Beispiel #12
0
        /// <summary>
        /// Finds a random recipe in the available recipe pool
        /// </summary>
        /// <returns></returns>
        private RecipeNode Fish()
        {
            RecipeNode recipeNode = null;

            // No pantry, fish through Recipe index
            if (this.pantryIngredients == null)
            {
                var tag          = (this.allowedTags == null) ? this.random.Next(Enum.GetNames(typeof(RecipeTag)).Length) : (int)this.allowedTags[this.random.Next(this.allowedTags.Length)];
                var recipesByTag = this.db.FindRecipesByTag(tag);

                // Nothing in that tag
                if (recipesByTag == null || recipesByTag.Length == 0)
                {
                    return(Fish());
                }

                var rnd = this.random.Next(recipesByTag.Length);
                recipeNode = recipesByTag[rnd];
            }
            else
            {
                // Fish through random pantry ingredient
                var rndIng  = this.random.Next(this.pantryIngredients.Length);
                var ingNode = this.pantryIngredients[rndIng];

                RecipeNode[] recipes;
                if (this.allowedTags != null && this.allowedTags.Length > 0)
                {
                    // Does this ingredient have any allowed tags?
                    if (!this.allowedTags.Intersect(ingNode.AvailableTags).Any())
                    {
                        return(this.Fish()); // No - Find something else
                    }

                    // Pick random tag from allowed tags (since this set is smaller, better to guess an available tag)
                    while (true)
                    {
                        var t      = this.random.Next(this.allowedTags.Length); // NOTE: Next will NEVER return MaxValue, so we don't subtract 1 from Length!
                        var rndTag = this.allowedTags[t];
                        recipes = ingNode.RecipesByTag[(int)rndTag] as RecipeNode[];

                        if (recipes != null)
                        {
                            break;
                        }
                    }
                }
                else
                {
                    // Just pick a random available tag
                    var rndTag = this.random.Next(ingNode.AvailableTags.Length);
                    var tag    = ingNode.AvailableTags[rndTag];
                    recipes = ingNode.RecipesByTag[(int)tag] as RecipeNode[];
                }

                if (recipes != null)
                {
                    var rndRecipe = this.random.Next(recipes.Length);
                    recipeNode = recipes[rndRecipe];
                }
            }

            // If there's a blacklist, make sure no ingredients are blacklisted otherwise try again
            if (this.ingBlacklist != null)
            {
                if (recipeNode != null)
                {
                    var ingredients = (IngredientUsage[])recipeNode.Ingredients;
                    for (var i = 0; i < ingredients.Length; i++)
                    {
                        if (this.ingBlacklist.Contains(ingredients[i].Ingredient))
                        {
                            return(this.Fish());
                        }
                    }
                }
            }

            // Discard if this recipe is to be avoided
            if (recipeNode != null && (this.profile.AvoidRecipe.HasValue && this.profile.AvoidRecipe.Value.Equals(recipeNode.RecipeId)))
            {
                return(this.Fish());
            }

            return(recipeNode);
        }