Skip to content

Commit 42ffe88

Browse files
Merge pull request #18 from Ahmed-Alqershi/add-trnspwl-example-with-formulations
add trnsport PWL formulations example
2 parents a52c8ce + f9d984a commit 42ffe88

1 file changed

Lines changed: 365 additions & 0 deletions

File tree

Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
1+
"""
2+
## GAMSSOURCE: https://www.gams.com/latest/gamslib_ml/libhtml/gamslib_trnspwl.html
3+
## LICENSETYPE: Demo
4+
## MODELTYPE: MIP, NLP
5+
## KEYWORDS: non linear programming, mixed integer linear programming, transportation problem, scheduling, economies of scale, non-convex objective, special ordered sets
6+
7+
8+
A Transportation Problem with discretized Economies of Scale (TRNSPWL)
9+
10+
This problem finds a least cost shipping schedule that meets
11+
requirements at markets and supplies at factories. This instance
12+
applies economies of scale which results in a non-convex
13+
objective. This is an extension of the trnsport model in the GAMS
14+
Model Library.
15+
16+
The original nonlinear term is "sum((i,j), c(i,j)*sqrt(x(i,j)))".
17+
We use the following discretization f(x) of sqrt(x)
18+
19+
For x<=50: f(x) = 1/sqrt(50)*x,
20+
for x>=400: f(x) = (sqrt(600)-sqrt(400))/200*(x-400) + sqrt(400)
21+
in between we discretize with linear interpolation between points
22+
23+
This discretization has some good properties:
24+
0) f(x) is a continuous function
25+
1) f(0)=0, otherwise we would pick up a fixed cost even for unused
26+
connections
27+
2) a fine representation in the reasonable range of shipments
28+
(between 50 and 400)
29+
3) f(x) underestimates sqrt in the area of x=0 to 600. Past that is
30+
overestimates sqrt.
31+
32+
The model is organized as follows:
33+
1) We set a starting point for the NLP solver so it will get stuck
34+
in local optimum that is not the global optimum.
35+
36+
2) We use three formulations for representing piecewise linear
37+
functions all based on the same discretization.
38+
39+
a) a formulation with SOS2 variables. This formulation mainly is
40+
based on the convex combination of neighboring
41+
points. Moreover, the domain of the discretization can be
42+
unbounded: we can assign a slope in the (potentially
43+
unbounded) first and last segment.
44+
45+
b) a formulation with SOS2 variables based on convex combinations
46+
of neighboring points. This formulation requires a bounded
47+
region for the discretization. Here we discretize between 0 and
48+
600.
49+
50+
c) a formuation with binary variables. This also requires the
51+
domain to be bounded, but it does not rely on the convex
52+
combination of neighboring points. There are examples, where
53+
this formulation solves much faster than the formulation b).
54+
55+
In this example x is clearly bounded by 0 from below and
56+
min(smax(i,a(i),smax(j,b(j)) from above, so formulation b and c
57+
are sufficient and perform better on this particular model and
58+
instance. We added the formulation a to demonstrate how to model
59+
an unbounded discretization, in case there are no derived
60+
bounds. The formulation a can be easily adjusted to accommodate
61+
problems where only one end of the discretization is unbounded.
62+
63+
3) We restart the non-convex NLP from the solution of the discretized
64+
model and hope that the NLP solver finds the global solution.
65+
66+
67+
Dantzig, G B, Chapter 3.3. In Linear Programming and Extensions.
68+
Princeton University Press, Princeton, New Jersey, 1963.
69+
"""
70+
71+
from __future__ import annotations
72+
73+
import numpy as np
74+
75+
import gamspy.formulations as formulations
76+
from gamspy import (
77+
Card,
78+
Container,
79+
Equation,
80+
Model,
81+
Options,
82+
Parameter,
83+
Problem,
84+
Sense,
85+
Set,
86+
Smax,
87+
Sum,
88+
Variable,
89+
)
90+
from gamspy.math import sqrt
91+
92+
93+
def main():
94+
m = Container()
95+
96+
# Sets
97+
i = Set(
98+
m,
99+
name="i",
100+
records=["seattle", "san-diego"],
101+
description="canning plants",
102+
)
103+
j = Set(
104+
m,
105+
name="j",
106+
records=["new-york", "chicago", "topeka"],
107+
description="markets",
108+
)
109+
110+
# Parameters
111+
a = Parameter(
112+
m,
113+
name="a",
114+
domain=i,
115+
records=np.array([350, 600]),
116+
description="capacity of plant i in cases",
117+
)
118+
119+
b = Parameter(
120+
m,
121+
name="b",
122+
domain=j,
123+
records=np.array([325, 300, 275]),
124+
description="demand at market j in cases",
125+
)
126+
127+
d = Parameter(
128+
m,
129+
name="d",
130+
domain=[i, j],
131+
records=np.array([[2.5, 1.7, 1.8], [2.5, 1.8, 1.4]]),
132+
description="distance in thousands of miles",
133+
)
134+
135+
f = Parameter(
136+
m,
137+
name="f",
138+
records=90,
139+
description="freight in dollars per case per thousand miles",
140+
)
141+
142+
c = Parameter(
143+
m,
144+
name="c",
145+
domain=[i, j],
146+
description="transport cost in thousands of dollars per case",
147+
)
148+
c[i, j] = f * d[i, j] / 1000
149+
150+
# Variables
151+
x = Variable(
152+
m,
153+
name="x",
154+
type="positive",
155+
domain=[i, j],
156+
description="shipment quantities in cases",
157+
)
158+
159+
# Equation
160+
161+
# Objective Function; total transportation costs in thousands of dollars
162+
cost = Sum([i, j], c[i, j] * sqrt(x[i, j]))
163+
164+
supply = Equation(
165+
m,
166+
name="supply",
167+
domain=i,
168+
description="observe supply limit at plant i",
169+
)
170+
demand = Equation(
171+
m,
172+
name="demand",
173+
domain=j,
174+
description="satisfy demand at market j",
175+
)
176+
177+
supply[i] = Sum(j, x[i, j]) <= a[i]
178+
179+
demand[j] = Sum(i, x[i, j]) >= b[j]
180+
181+
transport = Model(
182+
m,
183+
name="transport",
184+
equations=m.getEquations(),
185+
problem=Problem.NLP,
186+
sense=Sense.MIN,
187+
objective=cost,
188+
)
189+
190+
# Start the local NLP solver in a local solution that is not globally
191+
# optimal
192+
x.l["seattle", "chicago"] = 25
193+
x.l["seattle", "topeka"] = 275
194+
x.l["san-diego", "new-york"] = 325
195+
x.l["san-diego", "chicago"] = 275
196+
197+
localopt = Parameter(
198+
m,
199+
name="localopt",
200+
description="objective of local optimum that is not globally optimal",
201+
)
202+
203+
transport.solve(options=Options(nlp="conopt"))
204+
print(
205+
"Initial Objective Function Value: ",
206+
round(transport.objective_value, 3),
207+
)
208+
209+
localopt[...] = transport.objective_value
210+
211+
# The first model (formulation a) implements a piecewise linear
212+
# approximation based on the convex combination of neighboring points
213+
# using SOS2 variables with unbounded segments at the beginning and
214+
# end of the discretization
215+
# Sets
216+
s = Set(
217+
m,
218+
name="s",
219+
records=["slope0"] + [f"s{i}" for i in range(1, 7)] + ["slopeN"],
220+
description="SOS2 elements",
221+
)
222+
ss = Set(
223+
m,
224+
name="ss",
225+
domain=s,
226+
records=[f"s{i}" for i in range(1, 7)],
227+
description="sample points",
228+
)
229+
230+
# Parameters
231+
p = Parameter(m, name="p", domain=s, description="x coordinate of sample point")
232+
sqrtp = Parameter(
233+
m, name="sqrtp", domain=s, description="y coordinate of sample point"
234+
)
235+
xlow = Parameter(m, name="xlow", records=50)
236+
xhigh = Parameter(m, name="xhigh", records=400)
237+
xmax = Parameter(m, name="xmax")
238+
239+
xmax[...] = Smax(i, a[i])
240+
241+
if xmax.records.value[0] < xhigh.records.value[0]:
242+
raise Exception("xhigh too big")
243+
244+
if xlow.records.value[0] < 0:
245+
raise Exception("xlow less than 0")
246+
247+
# Equidistant sampling of the sqrt function with slopes at the beginning
248+
# and end
249+
p["slope0"] = -1
250+
p[ss] = xlow + (xhigh - xlow) / (Card(ss) - 1) * ss.off
251+
p["slopeN"] = 1
252+
253+
sqrtp["slope0"] = -1 / sqrt(xlow)
254+
sqrtp[ss] = sqrt(p[ss])
255+
sqrtp["slopeN"] = (sqrt(xmax) - sqrt(xhigh)) / (xmax - xhigh)
256+
257+
x_points = [record[-1] for record in p[ss].toList()]
258+
y_points = [record[-1] for record in sqrtp[ss].toList()]
259+
left_gradient = -sqrtp["slope0"].toList()[0]
260+
right_gradient = sqrtp["slopeN"].toList()[0]
261+
sqrtx, eqs = formulations.pwl_convexity_formulation(
262+
x,
263+
[-float("inf"), *x_points, float("inf")],
264+
[left_gradient, *y_points, right_gradient],
265+
using="sos2",
266+
)
267+
sqrtx.lo[...] = 0
268+
269+
# Alternatively, use the Graph API:
270+
# graph = formulations.PWLGraph(
271+
# list(zip(x_points, y_points, strict=True)),
272+
# left_gradient=left_gradient,
273+
# right_gradient=right_gradient,
274+
# )
275+
# sqrtx, eqs = formulations.pwlinear(
276+
# x, graph, method="convexity", using="sos2"
277+
# )
278+
279+
defobjdisc = Sum([i, j], c[i, j] * sqrtx[i, j])
280+
281+
trnsdiscA = Model(
282+
m,
283+
name="trnsdiscA",
284+
equations=[supply, demand, *eqs],
285+
problem="mip",
286+
sense=Sense.MIN,
287+
objective=defobjdisc,
288+
)
289+
290+
trnsdiscA.solve(options=Options(relative_optimality_gap=0))
291+
292+
# The next model (formulation b) uses the convex combinations of
293+
# neighboring points but requires the discretization to be bounded
294+
# (here we go from 0 to xmax).
295+
p["slope0"] = 0
296+
p[ss] = xlow + (xhigh - xlow) / (Card(ss) - 1) * ss.off
297+
p["slopeN"] = xmax
298+
sqrtp[s] = sqrt(p[s])
299+
300+
x_points = [0, *[record[-1] for record in p[ss].toList()], xmax.toValue()]
301+
y_points = [
302+
0,
303+
*[record[-1] for record in sqrtp[ss].toList()],
304+
sqrtp["slopeN"].toList()[0],
305+
]
306+
sqrtx, eqs = formulations.pwl_convexity_formulation(
307+
x, x_points, y_points, using="sos2"
308+
)
309+
310+
# Alternatively, use the Graph API:
311+
# graph = formulations.PWLGraph(list(zip(x_points, y_points, strict=True)))
312+
# sqrtx, eqs = formulations.pwlinear(
313+
# x, graph, method="convexity", using="sos2"
314+
# )
315+
316+
defobjdisc = Sum([i, j], c[i, j] * sqrtx[i, j])
317+
318+
trnsdiscB = Model(
319+
m,
320+
name="trnsdiscB",
321+
equations=[supply, demand, *eqs],
322+
problem="mip",
323+
sense=Sense.MIN,
324+
objective=defobjdisc,
325+
)
326+
327+
trnsdiscB.solve()
328+
329+
# The next model (formulation c) implements another formulation for a
330+
# piecewise linear function. We need to assume that the domain region
331+
# is bounded. We use the same discretization as in the previous
332+
# formulation.
333+
sqrtx, eqs = formulations.pwl_interval_formulation(x, x_points, y_points)
334+
335+
# Alternatively, use the Graph API with the graph defined above:
336+
# sqrtx, eqs = formulations.pwlinear(x, graph, method="interval")
337+
338+
defobjdisc = Sum([i, j], c[i, j] * sqrtx[i, j])
339+
340+
trnsdiscC = Model(
341+
m,
342+
name="trnsdiscC",
343+
equations=[supply, demand, *eqs],
344+
problem="mip",
345+
sense=Sense.MIN,
346+
objective=defobjdisc,
347+
)
348+
349+
trnsdiscC.solve()
350+
351+
# Now restart the local solver from this approximate point
352+
transport.solve(options=Options(nlp="conopt"))
353+
354+
print(
355+
"Improved Objective Function Value: ",
356+
round(transport.objective_value, 3),
357+
)
358+
359+
# Ensure that we are better off transport.objective_value
360+
if transport.objective_value - localopt.toValue() > 1e-6:
361+
raise Exception("we should get an improved transport.objective_value")
362+
363+
364+
if __name__ == "__main__":
365+
main()

0 commit comments

Comments
 (0)