from src.cocktails.utilities.ingredients_utilities import get_ingredients_info, format_ingredients, extract_ingredients, ingredients_per_type, bubble_ingredients import numpy as np from src.cocktails.utilities.other_scrubbing_utilities import print_recipe from src.cocktails.utilities.cocktail_utilities import get_cocktail_rep, get_profile, get_bunch_of_rep_keys from src.cocktails.utilities.glass_and_volume_utilities import glass_volume from src.cocktails.representation_learning.run import get_model from src.cocktails.pipeline.get_cocktail2affective_cluster import get_cocktail2affective_cluster from src.cocktails.config import COCKTAILS_CSV_DATA, FULL_COCKTAIL_REP_PATH, REPO_PATH, COCKTAIL_REP_CHKPT_PATH, RECIPE2FEATURES_PATH from src.cocktails.representation_learning.run_without_vae import get_model from src.cocktails.utilities.cocktail_category_detection_utilities import find_cocktail_sub_category import pandas as pd import torch import time device = 'cuda' if torch.cuda.is_available() else 'cpu' density_ingredients = np.loadtxt(COCKTAIL_REP_CHKPT_PATH + 'density_ingredients.txt') max_ingredients, ingredient_list, ind_alcohol = get_ingredients_info() min_ingredients = 2 factor_max = 1.2 # generated recipes can go up to 1.2 times the max quantity of the ingredient found in the dataset prep_model = get_model(RECIPE2FEATURES_PATH + 'multi_predictor/')[0] all_rep_path = FULL_COCKTAIL_REP_PATH all_reps = np.loadtxt(all_rep_path) experiment_dir = REPO_PATH + '/experiments/cocktails/' rep_keys = get_bunch_of_rep_keys()['custom'] dict_weights_mse_computation = {'end volume': .1, 'end sour': 2, 'end sweet': 2, 'end booze': 4, 'end bitter': 2, 'end fruit': 1, 'end herb': 1, 'end complex': 1, 'end spicy': 5, 'end oaky': 1, 'end fizzy': 10, 'end colorful': 1, 'end eggy': 10} assert sorted(dict_weights_mse_computation.keys()) == sorted(rep_keys) weights_mse_computation = np.array([dict_weights_mse_computation[k] for k in rep_keys]) weights_mse_computation /= weights_mse_computation.sum() data = pd.read_csv(COCKTAILS_CSV_DATA) preparation_list = sorted(set(data['category'])) glasses_list = sorted(set(data['glass'])) weights_perf_n_ing = {2:0.71, 3:0.81, 4:0.93, 5:1., 6:1.03, 7:1.08, 8:1.05} # weights_perf_n_ing = {2:0.75, 3:0.8, 4:0.95, 5:1.05, 6:1.05, 7:1.05, 8:1.05} min_ingredients_quantities_when_present = np.loadtxt(COCKTAIL_REP_CHKPT_PATH +'ingredients_min_quantities_when_present.txt') min_ingredients_quantities = np.loadtxt(COCKTAIL_REP_CHKPT_PATH +'ingredients_min_quantities.txt') max_ingredients_quantities = np.loadtxt(COCKTAIL_REP_CHKPT_PATH + 'ingredients_max_quantities.txt') min_cocktail_rep, max_cocktail_rep = np.loadtxt(COCKTAIL_REP_CHKPT_PATH +'cocktail_minmax_dim13_customkeys.txt') distrib_nb_ings_2_8 = np.loadtxt(COCKTAIL_REP_CHKPT_PATH + 'distrib_nb_ing.txt')[2:] def normalize_cocktail(cocktail_rep): return ((cocktail_rep - min_cocktail_rep) / (max_cocktail_rep - min_cocktail_rep) - 0.5) * 2 def denormalize_cocktail(cocktail_rep): return (cocktail_rep / 2 + 0.5) * (max_cocktail_rep - min_cocktail_rep) + min_cocktail_rep def normalize_ingredient_q_rep(ingredients_q): return (ingredients_q - min_ingredients_quantities_when_present) / (max_ingredients_quantities * factor_max - min_ingredients_quantities_when_present) COCKTAIL_REPS = normalize_cocktail(np.array([data[k] for k in rep_keys]).transpose()) assert np.abs(COCKTAIL_REPS - all_reps).sum() < 1e-8 cocktail2affective_cluster = get_cocktail2affective_cluster() original_affective_keys = get_bunch_of_rep_keys()['affective'] def sigmoid(x, shift, beta): return (1 / (1 + np.exp(-(x + shift) * beta)) - 0.5) * 2 def get_normalized_affective_cocktail_rep_from_normalized_cocktail_rep(cocktail_rep): indexes = np.array([rep_keys.index(key) for key in original_affective_keys]) cocktail_rep = cocktail_rep[indexes] cocktail_rep[0] = sigmoid(cocktail_rep[0], shift=0.05, beta=4) cocktail_rep[1] = sigmoid(cocktail_rep[1], shift=0.3, beta=5) cocktail_rep[2] = sigmoid(cocktail_rep[2], shift=0.15, beta=3) cocktail_rep[3] = sigmoid(cocktail_rep[3], shift=0.9, beta=20) cocktail_rep[4] = sigmoid(cocktail_rep[4], shift=0, beta=4) cocktail_rep[5] = sigmoid(cocktail_rep[5], shift=0.2, beta=3) cocktail_rep[6] = sigmoid(cocktail_rep[6], shift=0.5, beta=5) cocktail_rep[7] = sigmoid(cocktail_rep[7], shift=0.2, beta=6) return cocktail_rep class IndividualCocktail(): def __init__(self, pop_params, target, target_affective_cluster, genes_presence=None, genes_quantity=None, compute_perf=True, known_target_dict=None, run_hard_check=False): self.pop_params = pop_params self.n_genes = len(ingredient_list) self.max_ingredients = max_ingredients self.min_ingredients = min_ingredients self.mutation_params = pop_params['mutation_params'] self.dist = pop_params['dist'] self.target = target self.is_known = known_target_dict is not None self.known_target_dict = known_target_dict self.perf = None self.cocktail_rep = None self.affective_cluster = None self.target_affective_cluster = target_affective_cluster self.ing_list = np.array(ingredient_list) self.ing_set = set(ingredient_list) self.ing_ids_per_cat = dict(bubbles=set(self.get_ingredients_ids_from_list(bubble_ingredients)), liquor=set(self.get_ingredients_ids_from_list(ingredients_per_type['liquor'])), liqueur=set(self.get_ingredients_ids_from_list(ingredients_per_type['liqueur'])), citrus=set(self.get_ingredients_ids_from_list(ingredients_per_type['acid'] + ['orange juice'])), alcohol=set(ind_alcohol), sweeteners=set(self.get_ingredients_ids_from_list(ingredients_per_type['sweeteners'])), vermouth=set(self.get_ingredients_ids_from_list(ingredients_per_type['vermouth'])), bitters=set(self.get_ingredients_ids_from_list(ingredients_per_type['bitters'])), juice=set(self.get_ingredients_ids_from_list(ingredients_per_type['juice'])), acid=set(self.get_ingredients_ids_from_list(ingredients_per_type['acid'])), egg=set(self.get_ingredients_ids_from_list(['egg'])) ) if genes_presence is not None: assert len(genes_presence) == self.n_genes assert len(genes_quantity) == self.n_genes self.genes_presence = genes_presence self.genes_quantity = genes_quantity if compute_perf: self.compute_cocktail_rep() self.compute_perf() else: self.sample_initial_genes() self.compute_cocktail_rep() # self.make_recipe_fit_the_glass() self.compute_perf() # # # # # # # # # # # # # # # # # # # # # # # # # Sample initial genes with smart rules # # # # # # # # # # # # # # # # # # # # # # # # def sample_initial_genes(self): # rules: # - between min_ingredients and max_ingredients # - at most one type of bubbles # - at least one alcohol # - no egg without lime or lemon # - at most two liqueurs # - at most three liquors # - at most two sweetener self.genes_quantity = np.random.uniform(0, 1, size=self.n_genes) # holds quantities for each ingredient n_ingredients = np.random.choice(np.arange(min_ingredients, max_ingredients + 1), p=distrib_nb_ings_2_8) self.genes_presence = np.zeros(self.n_genes) # add one alchohol self.genes_presence[np.random.choice(ind_alcohol)] = 1 while self.get_ing_count() < n_ingredients: candidate_ids = self.get_candidate_ingredients_ids(self.genes_presence) probas = density_ingredients[candidate_ids] / np.sum(density_ingredients[candidate_ids]) self.genes_presence[np.random.choice(candidate_ids, p=probas)] = 1 def get_candidate_ingredients_ids(self, genes_presence): candidates = set(np.argwhere(genes_presence==0).flatten()) present_ids = set(np.argwhere(genes_presence==1).flatten()) if self.count_in_genes(present_ids, 'bubbles') >= 1: # at most one type of bubbles candidates = candidates - self.ing_ids_per_cat['bubbles'] if self.count_in_genes(present_ids, 'liquor') >= 3: # at most three liquors candidates = candidates - self.ing_ids_per_cat['liquor'] if self.count_in_genes(present_ids, 'liqueur') >= 2: # at most two liqueurs candidates = candidates - self.ing_ids_per_cat['liqueur'] if self.count_in_genes(present_ids, 'sweeteners') >= 2: # at most two sweetener candidates = candidates - self.ing_ids_per_cat['sweeteners'] if self.count_in_genes(present_ids, 'citrus') == 0: # no egg without lime or lemon candidates = candidates - self.ing_ids_per_cat['egg'] return np.array(sorted(candidates)) def count_in_genes(self, present_ids, keyword): if keyword == 'citrus': return len(present_ids & self.ing_ids_per_cat['citrus']) elif keyword == 'bubbles': return len(present_ids & self.ing_ids_per_cat['bubbles']) elif keyword == 'liquor': return len(present_ids & self.ing_ids_per_cat['liquor']) elif keyword == 'liqueur': return len(present_ids & self.ing_ids_per_cat['liqueur']) elif keyword == 'alcohol': return len(present_ids & self.ing_ids_per_cat['alcohol']) elif keyword == 'sweeteners': return len(present_ids & self.ing_ids_per_cat['sweeteners']) else: raise ValueError def get_ingredients_ids_from_list(self, ing_list): return [ingredient_list.index(ing) for ing in ing_list] def get_ing_count(self): return np.sum(self.genes_presence) # # # # # # # # # # # # # # # # # # # # # # # # # Compute cocktail representations # # # # # # # # # # # # # # # # # # # # # # # # def get_absent_ing(self): return np.argwhere(self.genes_presence==0).flatten() def get_present_ing(self): return np.argwhere(self.genes_presence==1).flatten() def get_ingredient_quantities(self): # unnormalize quantities to get real ones return (self.genes_quantity * (max_ingredients_quantities * factor_max - min_ingredients_quantities_when_present) + min_ingredients_quantities_when_present) * self.genes_presence def get_ing_and_q_from_genes(self): present_ings = self.get_present_ing() ing_quantities = self.get_ingredient_quantities() ingredients, quantities = [], [] for i_ing in present_ings: ingredients.append(ingredient_list[i_ing]) quantities.append(ing_quantities[i_ing]) return ingredients, quantities, ing_quantities def compute_cocktail_rep(self): # only call when genes have changes init_time = time.time() ingredients, quantities, ing_quantities = self.get_ing_and_q_from_genes() # compute cocktail category self.category = find_cocktail_sub_category(ingredients, quantities)[0] # print(f't1: {time.time() - init_time}') init_time = time.time() self.prep_type = self.get_prep_type(ing_quantities) # print(f't2: {time.time() - init_time}') init_time = time.time() cocktail_rep, self.end_volume, self.end_alcohol = get_cocktail_rep(self.prep_type, ingredients, quantities, keys=rep_keys[1:]) # volume is added later # print(f't3: {time.time() - init_time}') init_time = time.time() self.cocktail_rep = normalize_cocktail(cocktail_rep) # print(f't4: {time.time() - init_time}') init_time = time.time() self.glass = self.get_glass_type(ing_quantities) # print(f't5: {time.time() - init_time}') init_time = time.time() if self.is_known: assert np.abs(self.cocktail_rep - self.target).sum() < 1e-6 return self.cocktail_rep def get_prep_type(self, quantities=None): if self.is_known: return self.known_target_dict['prep_type'] else: if quantities is None: quantities = self.get_ingredient_quantities() if quantities[ingredient_list.index('egg')] > 0: prep_cat = 'egg_shaken' elif self.category in ['spirit_forward', 'simple_sour_with_juice', 'julep', 'duo', 'ancestral', 'complex_sour_with_juice']: # use hard coded rules for most obvious cases determined with the correlations_glass_cat_prep_script if self.category in ['ancestral', 'spirit_forward', 'duo']: prep_cat = 'stirred' elif self.category in ['complex_sour_with_juice', 'julep', 'simple_sour_with_juice']: prep_cat = 'shaken' else: raise ValueError else: output = prep_model(quantities, aux_str='prep_type').flatten() output[preparation_list.index('egg_shaken')] = -np.inf prep_cat = preparation_list[np.argmax(output)] return prep_cat def get_glass_type(self, quantities=None): if self.is_known: return self.known_target_dict['glass'] else: if self.category in ['collins', 'complex_highball', 'simple_highball', 'champagne_cocktail', 'complex_sour']: # use hard coded rules for most obvious cases determined with the correlations_glass_cat_prep_script if self.category in ['collins', 'complex_highball', 'simple_highball']: glass = 'collins' elif self.category in ['champagne_cocktail', 'complex_sour']: glass = 'coupe' else: if quantities is None: quantities = self.get_ingredient_quantities() output = prep_model(quantities, aux_str='glasses').flatten() glass = glasses_list[np.argmax(output)] return glass # # # # # # # # # # # # # # # # # # # # # # # # # Adapt recipe to fit the glass # # # # # # # # # # # # # # # # # # # # # # # # def is_too_large_for_glass(self): return self.end_volume > glass_volume[self.glass] * 0.80 def is_too_small_for_glass(self): return self.end_volume < glass_volume[self.glass] * 0.3 def scale_ing_quantities(self, present_ings, factor): qs = self.get_ingredient_quantities().copy() qs[present_ings] *= factor self.set_genes_from_quantities(present_ings, qs) def set_genes_from_quantities(self, present_ings, quantities): genes_quantity = np.clip((quantities - min_ingredients_quantities_when_present) / (factor_max * max_ingredients_quantities - min_ingredients_quantities_when_present), 0, 1) self.genes_quantity[present_ings] = genes_quantity[present_ings] def make_recipe_fit_the_glass(self): # check if citrus, if not remove egg present_ids = np.argwhere(self.genes_presence == 1).flatten() ing_list = self.ing_list[present_ids] present_ids = set(present_ids) if self.count_in_genes(present_ids, 'citrus') == 0 and 'egg' in ing_list: if self.genes_presence.sum() > 2: i_egg = ingredient_list.index('egg') self.genes_presence[i_egg] = 0. self.compute_cocktail_rep() i_trial = 0 present_ings = self.get_present_ing() while self.is_too_large_for_glass(): i_trial += 1 end_volume = self.end_volume desired_volume = glass_volume[self.glass] * 0.80 ratio = desired_volume / end_volume self.scale_ing_quantities(present_ings, factor=ratio) self.compute_cocktail_rep() if end_volume == self.end_volume: break if i_trial == 10: break while self.is_too_small_for_glass(): i_trial += 1 end_volume = self.end_volume desired_volume = glass_volume[self.glass] * 0.80 ratio = desired_volume / end_volume self.scale_ing_quantities(present_ings, factor=ratio) self.compute_cocktail_rep() if end_volume == self.end_volume: break if i_trial == 10: break # # # # # # # # # # # # # # # # # # # # # # # # # Compute performance # # # # # # # # # # # # # # # # # # # # # # # # def passes_checks(self): present_ids = np.argwhere(self.genes_presence==1).flatten() # ing_list = self.ing_list[present_ids] present_ids = set(present_ids) if len(present_ids) < 2 or len(present_ids) > 8: return False # if self.is_too_large_for_glass(): return False # if self.is_too_small_for_glass(): return False if self.end_alcohol < 0.05 or self.end_alcohol > 0.31: return False if self.count_in_genes(present_ids, 'sweeteners') > 2: return False if self.count_in_genes(present_ids, 'liqueur') > 2: return False if self.count_in_genes(present_ids, 'liquor') > 3: return False # if self.count_in_genes(present_ids, 'citrus') == 0 and 'egg' in ing_list: return False if self.count_in_genes(present_ids, 'bubbles') > 1: return False else: return True def get_affective_cluster(self): cocktail_rep_affective = get_normalized_affective_cocktail_rep_from_normalized_cocktail_rep(self.cocktail_rep) self.affective_cluster = cocktail2affective_cluster(cocktail_rep_affective)[0] return self.affective_cluster def does_affective_cluster_match(self): return True#self.get_affective_cluster() == self.target_affective_cluster def compute_perf(self): if not self.passes_checks(): self.perf = -100 else: if self.dist == 'mse': # self.perf = - np.sqrt(((self.cocktail_rep - self.target)**2).mean()) self.perf = - np.sqrt(np.dot((self.cocktail_rep - self.target)**2, weights_mse_computation)) self.perf *= weights_perf_n_ing[int(self.genes_presence.sum())] if not self.does_affective_cluster_match(): self.perf *= 2 else: raise NotImplemented # # # # # # # # # # # # # # # # # # # # # # # # # Mutations and crossover # # # # # # # # # # # # # # # # # # # # # # # # def get_child(self): time_dict = dict() init_time = time.time() child = IndividualCocktail(pop_params=self.pop_params, target_affective_cluster=self.target_affective_cluster, target=self.target, genes_presence=self.genes_presence.copy(), genes_quantity=self.genes_quantity.copy(), compute_perf=False) time_dict[' asexual child creation'] = [time.time() - init_time] init_time = time.time() this_time_dict = child.mutate() time_dict = self.update_time_dict(time_dict, this_time_dict) time_dict[' asexual child mutation'] = [time.time() - init_time] return child, time_dict def get_child_with(self, other_parent): time_dict = dict() init_time = time.time() new_genes_presence = np.zeros(self.n_genes) present_ing = self.get_present_ing() other_present_ing = other_parent.get_present_ing() new_genes_quantity = np.random.uniform(0, 1, size=self.n_genes) shared_ingredients = sorted(set(present_ing) & set(other_present_ing)) unique_ingredients_one = sorted(set(present_ing) - set(other_present_ing)) unique_ingredients_two = sorted(set(other_present_ing) - set(present_ing)) for i in shared_ingredients: new_genes_presence[i] = 1 new_genes_quantity[i] = (self.genes_quantity[i] + other_parent.genes_quantity[i]) / 2 time_dict[' crossover child creation'] = [time.time() - init_time] init_time = time.time() # add one alcohol if none present if len(set(np.argwhere(new_genes_presence==1).flatten()).intersection(ind_alcohol)) == 0: new_genes_presence[np.random.choice(ind_alcohol)] = 1 # up to here, we respect the constraints (assuming both parents do). candidate_genes = np.array(unique_ingredients_one + unique_ingredients_two) candidate_quantities = np.array([self.genes_quantity[i] for i in unique_ingredients_one] + [other_parent.genes_quantity[i] for i in unique_ingredients_two]) indexes = np.arange(len(candidate_genes)) np.random.shuffle(indexes) candidate_genes = candidate_genes[indexes] candidate_quantities = candidate_quantities[indexes] time_dict[' crossover prepare selection'] = [time.time() - init_time] init_time = time.time() # now let's try to add each of them while respecting the constraints for i in range(len(indexes)): if np.random.rand() < 0.5 or np.sum(new_genes_presence) < self.min_ingredients: # only try to add one every two ingredient ing_id = candidate_genes[i] q = candidate_quantities[i] new_genes_presence[ing_id] = 1 new_genes_quantity[ing_id] = q if np.sum(new_genes_presence) == self.max_ingredients: break time_dict[' crossover do selection'] = [time.time() - init_time] init_time = time.time() # create new child child = IndividualCocktail(pop_params=self.pop_params, target_affective_cluster=self.target_affective_cluster, target=self.target, genes_presence=new_genes_presence.copy(), genes_quantity=new_genes_quantity.copy(), compute_perf=False) time_dict[' crossover create child'] = [time.time() - init_time] init_time = time.time() this_time_dict = child.mutate() time_dict = self.update_time_dict(time_dict, this_time_dict) time_dict[' crossover child mutation'] = [time.time() - init_time] init_time = time.time() return child, time_dict def mutate(self): # self.print_recipe() time_dict = dict() # remove an ingredient init_time = time.time() present_ids = set(np.argwhere(self.genes_presence==1).flatten()) if np.random.rand() < self.mutation_params['p_remove_ing']: if self.get_ing_count() > self.min_ingredients: candidate_ings = self.get_present_ing() if self.count_in_genes(present_ids, 'alcohol') == 1: # make sure we keep at least one liquor candidate_ings = np.array(sorted(set(candidate_ings) - set(ind_alcohol))) index_to_remove = np.random.choice(candidate_ings) self.genes_presence[index_to_remove] = 0 time_dict[' mutation remove ing'] = [time.time() - init_time] init_time = time.time() # add an ingredient if np.random.rand() < self.mutation_params['p_add_ing']: if self.get_ing_count() < self.max_ingredients: candidate_ings = self.get_candidate_ingredients_ids(self.genes_presence.copy()) index_to_add = np.random.choice(candidate_ings, p=density_ingredients[candidate_ings] / np.sum(density_ingredients[candidate_ings])) self.genes_presence[index_to_add] = 1 time_dict[' mutation add ing'] = [time.time() - init_time] init_time = time.time() # replace ings by others from the same family if np.random.rand() < self.mutation_params['p_switch_ing']: i = np.random.choice(self.get_present_ing()) ing_str = ingredient_list[i] if ing_str not in ['sparkling wine', 'orange juice']: if ing_str in bubble_ingredients: candidates_ids = np.array(sorted(self.ing_ids_per_cat['bubbles'] - set([i]))) new_bubble = np.random.choice(candidates_ids, p=density_ingredients[candidates_ids] / np.sum(density_ingredients[candidates_ids])) self.genes_presence[i] = 0 self.genes_presence[new_bubble] = 1 self.genes_quantity[new_bubble] = self.genes_quantity[i] # copy quantity categories = ['acid', 'bitters', 'juice', 'liqueur', 'liquor', 'sweeteners', 'vermouth'] for cat in categories: if ing_str in ingredients_per_type[cat]: present_ings = self.get_present_ing() candidates_ids = np.array(sorted(self.ing_ids_per_cat[cat] - set([i]) - set(present_ings))) if len(candidates_ids) > 0: replacing_ing = np.random.choice(candidates_ids, p=density_ingredients[candidates_ids] / np.sum(density_ingredients[candidates_ids])) self.genes_presence[i] = 0 self.genes_presence[replacing_ing] = 1 self.genes_quantity[replacing_ing] = self.genes_quantity[i] # copy quantity break time_dict[' mutation switch ing'] = [time.time() - init_time] init_time = time.time() # add noise on ing quantity for i in self.get_present_ing(): if np.random.rand() < self.mutation_params['p_change_q']: self.genes_quantity[i] += np.random.randn() * self.mutation_params['delta_change_q'] self.genes_quantity = np.clip(self.genes_quantity, 0, 1) time_dict[' mutation change quantity'] = [time.time() - init_time] init_time = time.time() self.compute_cocktail_rep() time_dict[' mutation compute cocktail rep'] = [time.time() - init_time] init_time = time.time() # self.make_recipe_fit_the_glass() time_dict[' mutation check glass fit'] = [time.time() - init_time] init_time = time.time() self.compute_perf() time_dict[' mutation compute perf'] = [time.time() - init_time] init_time = time.time() stop = 1 return time_dict def update_time_dict(self, main_dict, new_dict): for k in new_dict.keys(): if k in main_dict.keys(): main_dict[k].append(np.sum(new_dict[k])) else: main_dict[k] = [np.sum(new_dict[k])] return main_dict # # # # # # # # # # # # # # # # # # # # # # # # # Get recipe and print # # # # # # # # # # # # # # # # # # # # # # # # def get_recipe(self, unit='mL', name=None): ing_quantities = self.get_ingredient_quantities() ingredients, quantities = [], [] for i_ing, q_ing in enumerate(ing_quantities): if q_ing > 0.8: ingredients.append(ingredient_list[i_ing]) quantities.append(round(q_ing)) recipe_str = format_ingredients(ingredients, quantities) recipe_str_readable = print_recipe(unit=unit, ingredient_str=recipe_str, name=name, to_print=False) return ingredients, quantities, recipe_str, recipe_str_readable def get_instructions(self): ing_quantities = self.get_ingredient_quantities() ingredients, quantities = [], [] for i_ing, q_ing in enumerate(ing_quantities): if q_ing > 0.8: ingredients.append(ingredient_list[i_ing]) quantities.append(round(q_ing)) str_out = 'Instructions:\n ' if 'mint' in ingredients: i_mint = ingredients.index('mint') n_leaves = quantities[i_mint] str_out += f'Add {n_leaves} mint leaves to a shaker, followed by an ice cube.\n Muddle the mint and ice together with a muddler.\n ' bubbles = ['sparkling wine', 'tonic', 'soda', 'ginger beer'] other_ings = [ing for ing in ingredients if ing not in ['egg', 'angostura', 'orange bitters'] + bubbles] if self.prep_type == 'built': str_out += 'Add a large ice cube in the glass.\n ' # add ingredients to pour str_out += 'Pour' for i, ing in enumerate(other_ings): if i == len(other_ings) - 2: str_out += f' {ing} and' elif i == len(other_ings) - 1: str_out += f' {ing}' else: str_out += f' {ing},' if self.prep_type in ['built'] and 'mint' not in ingredients: str_out += ' into the glass.\n ' else: str_out += ' into the shaker.\n ' if self.prep_type == 'egg_shaken' and 'egg' in ingredients: str_out += 'Add the egg white.\n Dry-shake for 15s (without ice), then fill with ice and shake for another 15s.\n Serve into the glass through a strainer.\n ' elif 'shaken' in self.prep_type: str_out += 'Fill with ice and shake for 15s.\n Serve into the glass through a strainer.\n ' elif self.prep_type == 'stirred': str_out += 'Add ice and stir the cocktail with a spoon for 15s.\n Serve into the glass through a strainer.\n ' elif self.prep_type == 'built': str_out += 'Stir two turns with a spoon.\n ' bubble_ing = [ing for ing in ingredients if ing in bubbles] if len(bubble_ing) > 0: str_out += f'Top up with ' for ing in bubble_ing: str_out += f'{ing}, ' str_out = str_out[:-2] + '.\n ' bitter_ing = [ing for ing in ingredients if ing in ['angostura', 'orange bitters']] if len(bitter_ing) > 0: if len(bitter_ing) == 1: q = quantities[ingredients.index(bitter_ing[0])] n_dashes = max(1, int(q / 0.6)) str_out += f'Add {n_dashes} dash' if n_dashes > 1: str_out += 'es' str_out += f' of {bitter_ing[0]}.\n ' elif len(bitter_ing) == 2: q = quantities[ingredients.index(bitter_ing[0])] n_dashes = max(1, int(q / 0.6)) str_out += f'Add {n_dashes} dash' if n_dashes > 1: str_out += 'es' str_out += f' of {bitter_ing[0]} and ' q = quantities[ingredients.index(bitter_ing[1])] n_dashes = max(1, int(q / 0.6)) str_out += f'{n_dashes} dash' if n_dashes > 1: str_out += 'es' str_out += f' of {bitter_ing[1]}.\n ' str_out += 'Enjoy!' return str_out def print_recipe(self, name=None): print(self.get_recipe(name)[3])