This notebook illustrates how to use Sympy, a Python-based symbolic computation system, to solve the LOCAL JOB SHOP problem from Hillier and Lieberman, Section 11.3. For students of Operations Research: the use of Sympy is not for examination. I include this example as you might find it useful for other work, and to give a short example for the dynamic programming paradigm to clarify the structure of an otherwise rather messy computation.
We begin by setting up the Sympy environment:
from sympy import init_session
init_session()
In Sympy, symbolic variables have to be declared. For us, the employment levels x1 (summer), x2 (autumn), and x3 (winter) are the decision variables. We declare them as symbolic variables as follows:
x1,x2,x3 = symbols('x_1 x_2 x_3')
We start at the final stage n=4 of the job shop problem, the transition of employment level from x3 in the winter to x∗4=255 in the spring. Since there is no excess employment cost for the spring, the only cost is the hire-or-fire expense. Thus, we get for f∗4:
f4s = 200*(255-x3)**2
At the pre-final stage n=3, the transition of employment from x2 in the autumn to x3 in the winter is the sum of three terms. First, the hire-or-fire expense, then the cost of over-employment, and the optimal future costs f∗4(x3). The resulting expression for f3(x2,x3) is the following:
f3 = 200*(x2-x3)**2 + 2000*(x3-200) + f4s
Since f3 is a quadratic function of x3, we find the minimum cost by taking the derivative and finding its root x∗3:
x3s = solve(diff(f3,x3),x3)[0]
x3s
The corresponding optimal cost f∗3(x2) is obtained by substituting x∗3 into the expression for f3:
f3s = f3.subs(x3,x3s)
simplify(f3s)
To conclude the stage n=3 computation, we verify that the computed x∗3 will always lie in the permissible interval x3∈[200,255]. Indeed, since x2∈[240,255], we find that x∗3 is in the following interval:
[x3s.subs(x2,240),x3s.subs(x2,255)]
We continue our backward recursion at stage n=2. Following the pattern above, we write out the expression for f2(x1,x2) and compute its minimum with respect to x2:
f2 = 200*(x1-x2)**2 + (x2-240)*2000 + f3s
x2s = solve(diff(f2,x2),x2)[0]
f2s = f2.subs(x2,x2s)
Noting that x1∈[220,255] and therefore
[x2s.subs(x1,220),x2s.subs(x1,255)]
so that x∗2 may lie outside its allowed interval x2∈[240,255], we compute the minimum value for x1 such that the x∗2 obtained above is valid:
solve(x2s-240,x1)
Thus, for x1∈[220,240], we need to set x∗2=240 and, correspondingly, f∗2(x1)=f2(x1,240).
f2s_alt = f2.subs(x2,240)
Now at stage n=1, we need to follow both stage n=2 cases separately. Since we know that x0=255 (this is again spring employment), we find for the case x1∈[240,255]:
f1 = 200*(255-x1)**2 + (x1-220)*2000 + f2s
x1s = solve(diff(f1,x1),x1)[0]
N(x1s)
Thus, the computed x∗1 lies in the interval of validity of the corresponding f∗2, so that f∗1 is given by
f1s = f1.subs(x1,x1s)
f1s
Likewise, for the alternative case x1∈[220,240], we compute
f1_alt = 200*(255-x1)**2 + (x1-220)*2000 + f2s_alt
x1s_alt = solve(diff(f1_alt,x1),x1)[0]
N(x1s_alt)
This value is to the right of the interval of validity. As the corresponding f∗1 is decreasing to the left of x1=245, the minimum is attained at x∗1=240, namely
f1.subs(x1,240)
This is larger than the first alternative, so we can discard this alternative case. Thus, the optimal cost over one year is f∗1=185000 while x∗1=247.5 as computed above. Further, x∗2 equals
x2s.subs(x1,x1s)
and x∗3 equals
N(x3s.subs(x2,x2s).subs(x1,x1s))