OR-Tools  9.6
cocktail_hour_mo.cc
Go to the documentation of this file.
1 // Copyright 2010-2022 Google LLC
2 // Licensed under the Apache License, Version 2.0 (the "License");
3 // you may not use this file except in compliance with the License.
4 // You may obtain a copy of the License at
5 //
6 // http://www.apache.org/licenses/LICENSE-2.0
7 //
8 // Unless required by applicable law or agreed to in writing, software
9 // distributed under the License is distributed on an "AS IS" BASIS,
10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 // See the License for the specific language governing permissions and
12 // limitations under the License.
13 
14 // Pick ingredients to buy to make the maximum number of cocktails.
15 //
16 // Given a list of cocktails, each of which is made from a list of ingredients,
17 // and a budget of how many ingredients you can buy, solve a MIP to pick a
18 // subset of the ingredients so that you can make the largest number of
19 // cocktails.
20 //
21 // This program can be run in three modes:
22 // text: Outputs the optimal set of ingredients and cocktails that can be
23 // produced as plain text to standard out.
24 // latex: Outputs a menu of the cocktails that can be made as LaTeX code to
25 // standard out.
26 // analysis: Computes the number of cocktails that can be made as a function
27 // of the number of ingredients for all values.
28 //
29 // In latex mode, the output can be piped directly to pdflatex, e.g.
30 // blaze run -c opt \
31 // ortools/math_opt/examples/cpp/cocktail_hour \
32 // -- --num_ingredients 10 --mode latex | pdflatex -output-directory /tmp
33 // will create a PDF in /tmp.
34 #include <iostream>
35 #include <limits>
36 
37 #include "absl/container/flat_hash_set.h"
38 #include "absl/status/status.h"
39 #include "absl/status/statusor.h"
40 #include "absl/strings/str_join.h"
41 #include "absl/strings/str_replace.h"
42 #include "absl/strings/string_view.h"
44 #include "ortools/base/logging.h"
45 #include "ortools/base/map_util.h"
49 
50 ABSL_FLAG(std::string, mode, "text",
51  "One of \"text\", \"latex\", or \"analysis\".");
52 ABSL_FLAG(int, num_ingredients, 10,
53  "How many ingredients to buy (ignored in analysis mode).");
54 ABSL_FLAG(std::vector<std::string>, existing_ingredients, {},
55  "Ingredients you already have (ignored in analysis mode).");
56 ABSL_FLAG(std::vector<std::string>, unavailable_ingredients, {},
57  "Ingredients you cannot get (ignored in analysis mode).");
58 ABSL_FLAG(std::vector<std::string>, required_cocktails, {},
59  "Cocktails you must be able to make (ignored in analysis mode).");
60 ABSL_FLAG(std::vector<std::string>, blocked_cocktails, {},
61  "Cocktails to exclude from the menu (ignored in analysis mode).");
62 
63 namespace {
64 
65 namespace math_opt = ::operations_research::math_opt;
66 
67 constexpr absl::string_view kIngredients[] = {"Amaro Nonino",
68  "All Spice Dram",
69  "Aperol",
70  "Bitters",
71  "Bourbon",
72  "Brandy",
73  "Campari",
74  "Cinnamon",
75  "Chambord",
76  "Cherry",
77  "Cloves",
78  "Cointreau",
79  "Coke",
80  "Cranberry",
81  "Creme de Cacao",
82  "Creme de Violette",
83  "Cucumber",
84  "Egg",
85  "Gin",
86  "Green Chartreuse",
87  "Heavy Cream",
88  "Lemon",
89  "Lillet Blanc",
90  "Lime",
91  "Luxardo",
92  "Mint",
93  "Orange",
94  "Orange Flower Water Extract",
95  "Orgeat",
96  "Pickle",
97  "Pineapple Juice",
98  "Pisco",
99  "Prosecco",
100  "Raspberry Vodka",
101  "Ruby Port",
102  "Rum",
103  "Seltzer",
104  "Simple Syrup",
105  "Sugar",
106  "Sweet Vermouth",
107  "Tequila",
108  "Tonic Water",
109  "Vodka"};
110 
111 constexpr std::size_t kIngredientsSize =
112  sizeof(kIngredients) / sizeof(kIngredients[0]);
113 
114 struct Cocktail {
115  std::string name;
116  std::vector<std::string> ingredients;
117 };
118 
119 std::vector<Cocktail> AllCocktails() {
120  return {
121  // Aperitifs
122  {.name = "Prosecco glass", .ingredients = {"Prosecco"}},
123  {.name = "Aperol Spritz", .ingredients = {"Prosecco", "Aperol"}},
124  {.name = "Chambord Spritz", .ingredients = {"Prosecco", "Chambord"}},
125  {.name = "Improved French 75",
126  .ingredients = {"Prosecco", "Vodka", "Lemon", "Simple Syrup"}},
127  // Quick and Simple
128  {.name = "Gin and Tonic", .ingredients = {"Gin", "Tonic Water", "Lime"}},
129  {.name = "Rum and Coke", .ingredients = {"Rum", "Coke"}},
130  {.name = "Improved Manhattan",
131  .ingredients = {"Bourbon", "Sweet Vermouth", "Bitters"}},
132  // Vodka
133 
134  // Serve with a sugared rim
135  {.name = "Lemon Drop",
136  .ingredients = {"Vodka", "Cointreau", "Lemon", "Simple Syrup"}},
137  // Shake, then float 2oz Prosecco after pouring
138  {.name = "Big Crush",
139  .ingredients = {"Raspberry Vodka", "Cointreau", "Lemon", "Chambord",
140  "Prosecco"}},
141  {.name = "Cosmopolitan",
142  .ingredients = {"Vodka", "Cranberry", "Cointreau", "Lime"}},
143  // A shot, chase with 1/3 of pickle spear
144  {.name = "Vodka/Pickle", .ingredients = {"Vodka", "Pickle"}},
145 
146  // Gin
147  {.name = "Last Word",
148  .ingredients = {"Gin", "Green Chartreuse", "Luxardo", "Lime"}},
149  {.name = "Corpse Reviver #2 (Lite)",
150  .ingredients = {"Gin", "Cointreau", "Lillet Blanc", "Lemon"}},
151  {.name = "Negroni", .ingredients = {"Gin", "Sweet Vermouth", "Campari"}},
152  // "Float" Creme de Violette (it will sink)
153  {.name = "Aviation",
154  .ingredients = {"Gin", "Luxardo", "Lemon", "Creme de Violette"}},
155 
156  // Bourbon
157  {.name = "Paper Plane",
158  .ingredients = {"Bourbon", "Aperol", "Amaro Nonino", "Lemon"}},
159  {.name = "Derby",
160  .ingredients = {"Bourbon", "Sweet Vermouth", "Lime", "Cointreau"}},
161  // Muddle sugar, water, bitters, and orange peel. Garnish with a Luxardo
162  // cherry (do not cheap out), spill cherry syrup generously in drink
163  {.name = "Old Fashioned",
164  .ingredients = {"Bourbon", "Sugar", "Bitters", "Orange", "Cherry"}},
165  {.name = "Boulevardier",
166  .ingredients = {"Bourbon", "Sweet Vermouth", "Campari"}},
167 
168  // Tequila
169  {.name = "Margarita", .ingredients = {"Tequila", "Cointreau", "Lime"}},
170  // Shake with chopped cucumber and strain. Garnish with cucumber.
171  {.name = "Midnight Cruiser",
172  .ingredients = {"Tequila", "Aperol", "Lime", "Pineapple Juice",
173  "Cucumber", "Simple Syrup"}},
174 
175  {.name = "Tequila shot", .ingredients = {"Tequila"}},
176  // Rum
177 
178  // Shake with light rum, float a dark rum on top.
179  {.name = "Pineapple Mai Tai",
180  .ingredients = {"Rum", "Lime", "Orgeat", "Cointreau",
181  "Pineapple Juice"}},
182  {.name = "Daiquiri", .ingredients = {"Rum", "Lime", "Simple Syrup"}},
183  {.name = "Mojito",
184  .ingredients = {"Rum", "Lime", "Simple Syrup", "Mint", "Seltzer"}},
185  // Add bitters generously. Invert half lime to form a cup, fill with
186  // Green Chartreuse and cloves. Float lime cup on drink and ignite.
187  {.name = "Kennedy",
188  .ingredients = {"Rum", "All Spice Dram", "Bitters", "Lime",
189  "Simple Syrup", "Cloves", "Green Chartreuse"}},
190 
191  // Egg
192 
193  {.name = "Pisco Sour",
194  .ingredients = {"Pisco", "Lime", "Simple Syrup", "Egg", "Bitters"}},
195  {.name = "Viana",
196  .ingredients = {"Ruby Port", "Brandy", "Creme de Cacao", "Sugar", "Egg",
197  "Cinnamon"}},
198  // Add cream last before shaking (and seltzer after shaking). Shake for 10
199  // minutes, no less.
200  {.name = "Ramos gin fizz",
201  .ingredients = {"Gin", "Seltzer", "Heavy Cream",
202  "Orange Flower Water Extract", "Egg", "Lemon", "Lime",
203  "Simple Syrup"}}};
204 }
205 
206 struct Menu {
207  std::vector<std::string> ingredients;
208  std::vector<Cocktail> cocktails;
209 };
210 
211 absl::StatusOr<Menu> SolveForMenu(
212  const int max_new_ingredients, const bool enable_solver_output,
213  const absl::flat_hash_set<std::string>& existing_ingredients,
214  const absl::flat_hash_set<std::string>& unavailable_ingredients,
215  const absl::flat_hash_set<std::string>& required_cocktails,
216  const absl::flat_hash_set<std::string>& blocked_cocktails) {
217  const std::vector<Cocktail> all_cocktails = AllCocktails();
218  math_opt::Model model("Cocktail hour");
219  absl::flat_hash_map<std::string, math_opt::Variable> ingredient_vars;
220  for (const absl::string_view ingredient : kIngredients) {
221  const double lb = existing_ingredients.contains(ingredient) ? 1.0 : 0.0;
222  const double ub = unavailable_ingredients.contains(ingredient) ? 0.0 : 1.0;
223  const math_opt::Variable v = model.AddIntegerVariable(lb, ub, ingredient);
224  gtl::InsertOrDie(&ingredient_vars, std::string(ingredient), v);
225  }
226  math_opt::LinearExpression ingredients_used;
227  for (const auto& [name, ingredient_var] : ingredient_vars) {
228  ingredients_used += ingredient_var;
229  }
230  model.AddLinearConstraint(ingredients_used <=
231  max_new_ingredients + existing_ingredients.size());
232 
233  absl::flat_hash_map<std::string, math_opt::Variable> cocktail_vars;
234  for (const Cocktail& cocktail : all_cocktails) {
235  const double lb = required_cocktails.contains(cocktail.name) ? 1.0 : 0.0;
236  const double ub = blocked_cocktails.contains(cocktail.name) ? 0.0 : 1.0;
237  const math_opt::Variable v =
238  model.AddIntegerVariable(lb, ub, cocktail.name);
239  for (const std::string& ingredient : cocktail.ingredients) {
240  model.AddLinearConstraint(v <=
241  gtl::FindOrDie(ingredient_vars, ingredient));
242  }
243  gtl::InsertOrDie(&cocktail_vars, cocktail.name, v);
244  }
245  math_opt::LinearExpression cocktails_made;
246  for (const auto& [name, cocktail_var] : cocktail_vars) {
247  cocktails_made += cocktail_var;
248  }
249  model.Maximize(cocktails_made);
250  const math_opt::SolveArguments args = {
251  .parameters = {.enable_output = enable_solver_output}};
253  math_opt::Solve(model, math_opt::SolverType::kGscip, args));
254 
255  switch (result.termination.reason) {
256  case math_opt::TerminationReason::kOptimal:
257  case math_opt::TerminationReason::kFeasible:
258  break;
259  default:
261  << "failed to find a solution: " << result.termination;
262  }
263  Menu menu;
264  for (const absl::string_view ingredient : kIngredients) {
265  if (result.variable_values().at(ingredient_vars.at(ingredient)) > 0.5) {
266  menu.ingredients.push_back(std::string(ingredient));
267  }
268  }
269  for (const Cocktail& cocktail : all_cocktails) {
270  if (result.variable_values().at(cocktail_vars.at(cocktail.name)) > 0.5) {
271  menu.cocktails.push_back(cocktail);
272  }
273  }
274  return menu;
275 }
276 
277 absl::flat_hash_set<std::string> SetFromVec(
278  const std::vector<std::string>& vec) {
279  return {vec.begin(), vec.end()};
280 }
281 
282 absl::Status AnalysisMode() {
283  std::cout << "Considering " << AllCocktails().size() << " cocktails and "
284  << kIngredientsSize << " ingredients." << std::endl;
285  std::cout << "Solving for number of cocktails that can be made as a function "
286  "of number of ingredients"
287  << std::endl;
288 
289  std::cout << "ingredients | cocktails" << std::endl;
290  for (int i = 1; i <= kIngredientsSize; ++i) {
291  const absl::StatusOr<Menu> menu = SolveForMenu(
292  i, false, /*existing_ingredients=*/{}, /*unavailable_ingredients=*/{},
293  /*required_cocktails=*/{}, /*blocked_cocktails=*/{});
294  RETURN_IF_ERROR(menu.status())
295  << "Failure when solving for " << i << " ingredients";
296  std::cout << i << " | " << menu->cocktails.size() << std::endl;
297  }
298  return absl::OkStatus();
299 }
300 
301 std::string ExportToLaTeX(const std::vector<Cocktail>& cocktails,
302  const std::string& title = "Cocktail Hour") {
303  std::vector<std::string> lines;
304  lines.push_back("\\documentclass{article}");
305  lines.push_back("\\usepackage{fullpage}");
306  lines.push_back("\\linespread{2}");
307  lines.push_back("\\begin{document}");
308  lines.push_back("\\begin{center}");
309  lines.push_back(absl::StrCat("\\begin{Huge}", title, "\\end{Huge}"));
310  lines.push_back("");
311  for (const Cocktail& cocktail : cocktails) {
312  lines.push_back(absl::StrCat(cocktail.name, "---{\\em ",
313  absl::StrJoin(cocktail.ingredients, ", "),
314  "}"));
315  lines.push_back("");
316  }
317  lines.push_back("\\end{center}");
318  lines.push_back("\\end{document}");
319 
320  return absl::StrReplaceAll(absl::StrJoin(lines, "\n"), {{"#", "\\#"}});
321 }
322 
323 absl::Status Main() {
324  const std::string mode = absl::GetFlag(FLAGS_mode);
325  CHECK(absl::flat_hash_set<std::string>({"text", "latex", "analysis"})
326  .contains(mode))
327  << "Unexpected mode: " << mode;
328 
329  // We are in analysis mode.
330  if (mode == "analysis") {
331  return AnalysisMode();
332  }
333 
335  Menu menu,
336  SolveForMenu(absl::GetFlag(FLAGS_num_ingredients), mode == "text",
337  SetFromVec(absl::GetFlag(FLAGS_existing_ingredients)),
338  SetFromVec(absl::GetFlag(FLAGS_unavailable_ingredients)),
339  SetFromVec(absl::GetFlag(FLAGS_required_cocktails)),
340  SetFromVec(absl::GetFlag(FLAGS_blocked_cocktails))),
341  _ << "error when solving for optimal set of ingredients");
342 
343  // We are in latex mode.
344  if (mode == "latex") {
345  std::cout << ExportToLaTeX(menu.cocktails) << std::endl;
346  return absl::OkStatus();
347  }
348 
349  // We are in text mode
350  std::cout << "Considered " << AllCocktails().size() << " cocktails and "
351  << kIngredientsSize << " ingredients." << std::endl;
352  std::cout << "Solution has " << menu.ingredients.size()
353  << " ingredients to make " << menu.cocktails.size() << " cocktails."
354  << std::endl
355  << std::endl;
356 
357  std::cout << "Ingredients:" << std::endl;
358  for (const std::string& ingredient : menu.ingredients) {
359  std::cout << " " << ingredient << std::endl;
360  }
361  std::cout << "Cocktails:" << std::endl;
362  for (const Cocktail& cocktail : menu.cocktails) {
363  std::cout << " " << cocktail.name << std::endl;
364  }
365  return absl::OkStatus();
366 }
367 
368 } // namespace
369 
370 int main(int argc, char** argv) {
371  InitGoogle(argv[0], &argc, &argv, true);
372  const absl::Status status = Main();
373  if (!status.ok()) {
374  LOG(QFATAL) << status;
375  }
376  return 0;
377 }
#define ASSIGN_OR_RETURN(lhs, rexpr)
#define RETURN_IF_ERROR(expr)
int main(int argc, char **argv)
ABSL_FLAG(std::string, mode, "text", "One of \"text\", \"latex\", or \"analysis\".")
const std::string name
absl::Status status
Definition: g_gurobi.cc:41
GRBmodel * model
void InitGoogle(const char *usage, int *argc, char ***argv, bool deprecated)
Definition: init_google.h:34
void InsertOrDie(Collection *const collection, const typename Collection::value_type &value)
Definition: map_util.h:154
const Collection::value_type::second_type & FindOrDie(const Collection &collection, const typename Collection::value_type::first_type &key)
Definition: map_util.h:206
absl::StatusOr< SolveResult > Solve(const Model &model, const SolverType solver_type, const SolveArguments &solve_args, const SolverInitArguments &init_args)
StatusBuilder InternalErrorBuilder()
const VariableMap< double > & variable_values() const
#define OR_ASSIGN_OR_RETURN3(lhs, rexpr, error_expression)