OR-Tools  9.2
cocktail_hour.cc
Go to the documentation of this file.
1// Copyright 2010-2021 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/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/flags/parse.h"
39#include "absl/flags/usage.h"
40#include "absl/status/status.h"
41#include "absl/status/statusor.h"
42#include "absl/strings/str_join.h"
43#include "absl/strings/str_replace.h"
44#include "absl/strings/string_view.h"
49
50ABSL_FLAG(std::string, mode, "text",
51 "One of \"text\", \"latex\", or \"analysis\".");
52ABSL_FLAG(int, num_ingredients, 10,
53 "How many ingredients to buy (ignored in analysis mode).");
54ABSL_FLAG(std::vector<std::string>, existing_ingredients, {},
55 "Ingredients you already have (ignored in analysis mode).");
56ABSL_FLAG(std::vector<std::string>, unavailable_ingredients, {},
57 "Ingredients you cannot get (ignored in analysis mode).");
58ABSL_FLAG(std::vector<std::string>, required_cocktails, {},
59 "Cocktails you must be able to make (ignored in analysis mode).");
60ABSL_FLAG(std::vector<std::string>, blocked_cocktails, {},
61 "Cocktails to exclude from the menu (ignored in analysis mode).");
62
63namespace {
64
65namespace math_opt = ::operations_research::math_opt;
66
67constexpr 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
111constexpr std::size_t kIngredientsSize =
112 sizeof(kIngredients) / sizeof(kIngredients[0]);
113
114struct Cocktail {
115 std::string name;
116 std::vector<std::string> ingredients;
117};
118
119std::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
206struct Menu {
207 std::vector<std::string> ingredients;
208 std::vector<Cocktail> cocktails;
209};
210
211absl::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 // Check that the problem has an optimal solution.
256 QCHECK_EQ(result.termination.reason, math_opt::TerminationReason::kOptimal)
257 << "Failed to find an optimal solution: " << result.termination;
258
259 Menu menu;
260 for (const absl::string_view ingredient : kIngredients) {
261 if (result.variable_values().at(ingredient_vars.at(ingredient)) > 0.5) {
262 menu.ingredients.push_back(std::string(ingredient));
263 }
264 }
265 for (const Cocktail& cocktail : all_cocktails) {
266 if (result.variable_values().at(cocktail_vars.at(cocktail.name)) > 0.5) {
267 menu.cocktails.push_back(cocktail);
268 }
269 }
270 return menu;
271}
272
273absl::flat_hash_set<std::string> SetFromVec(
274 const std::vector<std::string>& vec) {
275 return {vec.begin(), vec.end()};
276}
277
278absl::Status AnalysisMode() {
279 std::cout << "Considering " << AllCocktails().size() << " cocktails and "
280 << kIngredientsSize << " ingredients." << std::endl;
281 std::cout << "Solving for number of cocktails that can be made as a function "
282 "of number of ingredients"
283 << std::endl;
284
285 std::cout << "ingredients | cocktails" << std::endl;
286 for (int i = 1; i <= kIngredientsSize; ++i) {
287 const absl::StatusOr<Menu> menu = SolveForMenu(
288 i, false, /*existing_ingredients=*/{}, /*unavailable_ingredients=*/{},
289 /*required_cocktails=*/{}, /*blocked_cocktails=*/{});
290 RETURN_IF_ERROR(menu.status())
291 << "Failure when solving for " << i << " ingredients";
292 std::cout << i << " | " << menu->cocktails.size() << std::endl;
293 }
294 return absl::OkStatus();
295}
296
297std::string ExportToLaTeX(const std::vector<Cocktail>& cocktails,
298 const std::string& title = "Cocktail Hour") {
299 std::vector<std::string> lines;
300 lines.push_back("\\documentclass{article}");
301 lines.push_back("\\usepackage{fullpage}");
302 lines.push_back("\\linespread{2}");
303 lines.push_back("\\begin{document}");
304 lines.push_back("\\begin{center}");
305 lines.push_back(absl::StrCat("\\begin{Huge}", title, "\\end{Huge}"));
306 lines.push_back("");
307 for (const Cocktail& cocktail : cocktails) {
308 lines.push_back(absl::StrCat(cocktail.name, "---{\\em ",
309 absl::StrJoin(cocktail.ingredients, ", "),
310 "}"));
311 lines.push_back("");
312 }
313 lines.push_back("\\end{center}");
314 lines.push_back("\\end{document}");
315
316 return absl::StrReplaceAll(absl::StrJoin(lines, "\n"), {{"#", "\\#"}});
317}
318
319void RealMain() {
320 const std::string mode = absl::GetFlag(FLAGS_mode);
321 CHECK(absl::flat_hash_set<std::string>({"text", "latex", "analysis"})
322 .contains(mode))
323 << "Unexpected mode: " << mode;
324
325 // We are in analysis mode.
326 if (mode == "analysis") {
327 const absl::Status status = AnalysisMode();
328 if (!status.ok()) {
329 LOG(QFATAL) << status;
330 }
331 return;
332 }
333
334 absl::StatusOr<Menu> menu =
335 SolveForMenu(absl::GetFlag(FLAGS_num_ingredients), mode == "text",
336 SetFromVec(absl::GetFlag(FLAGS_existing_ingredients)),
337 SetFromVec(absl::GetFlag(FLAGS_unavailable_ingredients)),
338 SetFromVec(absl::GetFlag(FLAGS_required_cocktails)),
339 SetFromVec(absl::GetFlag(FLAGS_blocked_cocktails)));
340 if (!menu.ok()) {
341 LOG(QFATAL) << "Error when solving for optimal set of ingredients: "
342 << menu.status();
343 }
344
345 // We are in latex mode.
346 if (mode == "latex") {
347 std::cout << ExportToLaTeX(menu->cocktails) << std::endl;
348 return;
349 }
350
351 // We are in text mode
352 std::cout << "Considered " << AllCocktails().size() << " cocktails and "
353 << kIngredientsSize << " ingredients." << std::endl;
354 std::cout << "Solution has " << menu->ingredients.size()
355 << " ingredients to make " << menu->cocktails.size()
356 << " cocktails." << std::endl
357 << std::endl;
358
359 std::cout << "Ingredients:" << std::endl;
360 for (const std::string& ingredient : menu->ingredients) {
361 std::cout << " " << ingredient << std::endl;
362 }
363 std::cout << "Cocktails:" << std::endl;
364 for (const Cocktail& cocktail : menu->cocktails) {
365 std::cout << " " << cocktail.name << std::endl;
366 }
367}
368
369} // namespace
370
371int main(int argc, char** argv) {
373 absl::ParseCommandLine(argc, argv);
374 RealMain();
375 return 0;
376}
#define CHECK(condition)
Definition: base/logging.h:495
#define LOG(severity)
Definition: base/logging.h:420
#define QCHECK_EQ
Definition: base/logging.h:40
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:35
GRBmodel * model
void InitGoogleLogging(const char *argv0)
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)
Definition: solve.cc:155
#define ASSIGN_OR_RETURN(lhs, rexpr)
Definition: status_macros.h:48
#define RETURN_IF_ERROR(expr)
Definition: status_macros.h:29
const VariableMap< double > & variable_values() const