OR-Tools  9.3
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/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"
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 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
277absl::flat_hash_set<std::string> SetFromVec(
278 const std::vector<std::string>& vec) {
279 return {vec.begin(), vec.end()};
280}
281
282absl::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
301std::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
323absl::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
370int 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 CHECK(condition)
Definition: base/logging.h:495
#define LOG(severity)
Definition: base/logging.h:420
#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:35
GRBmodel * model
void InitGoogle(const char *usage, int *argc, char ***argv, bool deprecated)
Definition: init_google.h:32
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:94
StatusBuilder InternalErrorBuilder()
const VariableMap< double > & variable_values() const
#define OR_ASSIGN_OR_RETURN3(lhs, rexpr, error_expression)