The Book of Gehn

Blending Whisky with Z3

April 29, 2021

A whisky producer uses three kind of licor to make their whisky: \(A\), \(B\) and \(C\).

Three kind of whisky can be made using the licor in the following proportions:

Whisky Sales Price Recipe
\(E\) \(680\) No less than 60% of \(A\), no more than 20% of \(C\)
\(F\) \(570\) No less than 15% of \(A\), no more than 80% of \(C\)
\(G\) \(450\) No more than 50% of \(C\)

The producer has the following stock and price list from its licor supplier:

Licor Stock Price
\(A\) \(2000\) \(700\)
\(B\) \(2500\) \(500\)
\(C\) \(1200\) \(400\)

Note: the prices are in $ per liter and the stock are in liters.

The goal is to maximize the profit.

Bad plan: non-linear system

Let’s begin with the bounds of each licor:

$$ A_E E + A_F F + A_G G = A <= 2000 \\ B_E E + B_F F + B_G G = B <= 2500 \\ C_E E + C_F F + C_G G = C <= 1200 \\ $$

We introduced a new variable per combination of licor and whisky: \(A_E\) stands for the ratio of licor \(A\) used in the whisky B, \(B_F\) stands for the ratio of licor \(B\) used in the whisky \(F\) and so on.

These variables are in liters of licor per liter of whisky (or they are unit-less if you prefer).

And we are fuck.

These three equations are not longer linear because we have the product of two variable (\(A_E E\) for example).

We need to rethink our strategy.

The blending

Think in \(A_E\) not as the ratio but as the amount of liters itself of licor \(A\) used in \(E\).

This is a blending problem where some variables represent a part, fraction or subcomponent of another.

$$ A_E + A_F + A_G = A <= 2000 \\ B_E + B_F + B_G = B <= 2500 \\ C_E + C_F + C_G = C <= 1200 \\ $$

Now the system is linear again.

What about the restrictions of each recipe?

It would be wonderful to put something like this that it is a literal translation of the problem into inequalities:

$$ 0.6 <= A_E/E <= 1 \\ 0 <= C_E/E <= 0.2 \\ --- \\ 0.15 <= A_F/F <= 1 \\ 0 <= C_F/F <= 0.8 \\ --- \\ 0 <= C_G/G <= 0.5 \\ $$

But again, that makes the system non-linear.

Instead we do the following:

$$ 0.6 E <= A_E <= 1 E \\ 0 E <= C_E <= 0.2 E \\ --- \\ 0.15 F <= A_F <= 1 F \\ 0 F <= C_F <= 0.8 F \\ --- \\ 0 G <= C_G <= 0.5 G \\ $$

We complete the recipe of each whisky with the remaining licor:

$$ A_E + B_E + C_E = E \\ A_F + B_F + C_F = F \\ A_G + B_G + C_G = G \\ $$

So what did we do? We split each licor (input \(I\)) into \(N\) blending variables (\(I_o\)), one for each whisky type (output \(O\)).

The goal

And finally, this is the goal to maximize:

$$ Z = max\{680 E + 570 F + 450 G - 700 A - 500 B - 400 C \} $$

Find the optimum with Z3

Setup the engine, create the variables and ensure that them are non-negative.

>>> from z3 import Reals, Optimize
>>> s = Optimize()

>>> A, B, C, E, F, G = Reals('A B C E F G')
>>> A_E, A_F, A_G = Reals('A_E A_F A_G')
>>> B_E, B_F, B_G = Reals('B_E B_F B_G')
>>> C_E, C_F, C_G = Reals('C_E C_F C_G')

>>> s.add(*[x >= 0 for x in [A, B, C, E, F, G]])
>>> s.add(*[x >= 0 for x in [A_E, A_F, A_G]])
>>> s.add(*[x >= 0 for x in [B_E, B_F, B_G]])
>>> s.add(*[x >= 0 for x in [C_E, C_F, C_G]])

Limit the amount of supplies

>>> s.add(
...     A <= 2000,
...     B <= 2500,
...     C <= 1200
... )

Split the licor for each whisky

>>> s.add(
...     A_E + A_F + A_G == A,
...     B_E + B_F + B_G == B,
...     C_E + C_F + C_G == C
... )

Blending rules:

>>> s.add(
...     # minimum amount of licor for whisky E
...     0.6 * E <= A_E,
...     0 <= C_E,
...     # maximum amount of licor for whisky E
...     A_E <= E,
...     C_E <= 0.2 * E,
...
...     # minimum amount of licor for whisky F
...     0.15 * F <= A_F,
...     0 <= C_F,
...     # maximum amount of licor for whisky F
...     A_F <= F,
...     C_F <= 0.8 * F,
...
...     # the same for whisky G
...     0 <= C_G,
...     C_G <= 0.5 * G
... )

>>> s.add(
...     A_E + B_E + C_E == E,
...     A_F + B_F + C_F == F,
...     A_G + B_G + C_G == G
... )

Profit!

>>> costs = A * 700 + B * 500 + C * 400
>>> income = E * 680 + F * 570 + G * 450
>>> profit = s.maximize(income - costs)

>>> s.check()
sat

>>> profit.value()
3590000/9

>>> m = s.model()
>>> print("Licor:", "A =", m[A], "B =", m[B], "C =", m[C])
Licor: A = 2000 B = 2500 C = 1200

>>> # We use m[.] to retrieve simple variables
>>> print("Whisky:", "E =", m[E], "F =", m[F], "G =", m[G])
Whisky: E = 22900/9 F = 28400/9 G = 0

>>> # We use m.eval() to evaluate complex expressions in the model context
>>> print("Costs =", m.eval(costs), "Income =", m.eval(income), "Profit =", profit.value())
Costs = 3130000 Income = 31760000/9 Profit = 3590000/9

Related tags: z3, smt, sat, solver, linear optimization, blending

Blending Whisky with Z3 - April 29, 2021 - Martin Di Paola