Quick Start Guide#
Introduction#
This quick start guide is designed to provide you with a concise overview of GAMSPy and its key features. By the end of this guide, you’ll have a solid understanding of how to create basic mathematical models using GAMSPy. For more advanced features, we recommend exploring our comprehensive user guide and the extensive model library.
While not mandatory, having a basic understanding of Python programming and familiarity with the Pandas library. will be helpful in following this tutorial.
A Transportation Problem#
In this guide, we’ll delve into an example of the transportation problem. This classic scenario involves managing supplies from various plants to meet demands at multiple markets for a single commodity. Additionally, we have the unit costs associated with shipping the commodity from plants to markets. The fundamental economic question here is:
How can we optimize the shipment quantities between each plant and market to minimize the total transport cost?
The problem’s algebraic representation typically takes the following format:
Indices (Sets):
\(i\) = plants
\(j\) = markets
Given Data (Parameters):
\(a_i\) = supply of commodity at plant \(i\) (in cases)
\(b_j\) = demand for commodity at market \(j\) (in cases)
\(c_{ij}\) = cost per unit of shipment between plant \(i\) and market \(j\)
Decision Variables:
\(x_{ij}\) = amount of commodity to ship from plant \(i\) to market \(j\) where \(x_{ij} \ge 0\) for all \(i,j\)
Constraints:
Observe supply limit at plant \(i\): \(\sum_j x_{ij} \le a_i \: \forall i\)
Satisfy demand at market \(j\): \(\sum_i x_{ij} \ge b_j \: \forall j\)
Objective Function: Minimize \(\sum_i \sum_j c_{ij} \cdot x_{ij}\)
Data#
Before we dive into the optimization process, let’s handle our data using the Pandas library. We’ll begin by organizing the necessary information, which we will subsequently feed into our optimization model.
[1]:
import pandas as pd
capacities = pd.DataFrame(
[["seattle", 350], ["san-diego", 600]], columns=["city", "capacity"]
).set_index("city")
capacities
[1]:
capacity | |
---|---|
city | |
seattle | 350 |
san-diego | 600 |
[2]:
demands = pd.DataFrame(
[["new-york", 325], ["chicago", 300], ["topeka", 275]], columns=["city", "demand"]
).set_index("city")
demands
[2]:
demand | |
---|---|
city | |
new-york | 325 |
chicago | 300 |
topeka | 275 |
[3]:
distances = pd.DataFrame(
[
["seattle", "new-york", 2.5],
["seattle", "chicago", 1.7],
["seattle", "topeka", 1.8],
["san-diego", "new-york", 2.5],
["san-diego", "chicago", 1.8],
["san-diego", "topeka", 1.4],
],
columns=["from", "to", "distance"],
).set_index(["from", "to"])
distances
[3]:
distance | ||
---|---|---|
from | to | |
seattle | new-york | 2.5 |
chicago | 1.7 | |
topeka | 1.8 | |
san-diego | new-york | 2.5 |
chicago | 1.8 | |
topeka | 1.4 |
[4]:
freight_cost = 90
Symbol Declaration#
In line with our systematic breakdown of the transportation problem into sets, parameters, variables, and constraints, we will adopt a similar approach to define the problem as a GAMSPy Model
. To do so, it is essential to import the gamspy
library initially.
[5]:
from gamspy import Container, Set, Parameter, Variable, Equation, Model, Sum, Sense
Container#
Before we proceed further, let’s create a Container
to encapsulate all the relevant information for our GAMSPy Model
. This Container
acts as a centralized hub, gathering essential data, sets, parameters, variables, and constraints, providing a clear structure for our optimization problem.
[6]:
m = Container()
Sets#
Sets serve as the fundamental building blocks of a GAMSPy Model
, directly corresponding to the indices in the algebraic representations of models. In our transportation problem context, we have defined the following indices:
\(i\) = plants
\(j\) = markets
For detailed guidance on using sets, please refer to the set section of our user guide.
There a two ways to declare sets:
Separate declaration and data assignment
Combine declaration and data assignment
Separate declaration and data assignment#
[7]:
i = Set(container=m, name="i", description="plants")
i.setRecords(capacities.index)
Combine declaration and data assignment#
[8]:
j = Set(container=m, name="j", description="markets", records=demands.index)
The effect of using the above Set
statements is that we declared two sets, namely \(i\) and \(j\). Additionally, we provided descriptions to elaborate on their meaning, enhancing the readability of our Model
. Lastly, we assigned members to the sets, establishing a clear connection between the abstract sets and their real-world counterparts.
\(i\) = {Seattle, San Diego}
\(j\) = {New York, Chicago, Topeka}
To verify the content of a set, you can use <set name>.records
.
[9]:
i.records
[9]:
uni | element_text | |
---|---|---|
0 | seattle | |
1 | san-diego |
[10]:
j.records
[10]:
uni | element_text | |
---|---|---|
0 | new-york | |
1 | chicago | |
2 | topeka |
Parameters#
Declaring parameters involves using Parameter
. Each parameter is assigned a name and a description. Note that parameter \(a_i\) is indexed by \(i\). To accommodate these indices, we include the domain
attribute, pointing to the corresponding set.
It is worth mentioning that, similar to sets, you have the flexibility to either combine or separate the declaration and data assignment steps. For convenience, we will proceed by combining the declaration and data assignment.
[11]:
a = Parameter(
container=m,
name="a",
domain=i,
description="supply of commodity at plant i (in cases)",
records=capacities.reset_index(),
)
a.records
[11]:
city | value | |
---|---|---|
0 | seattle | 350.0 |
1 | san-diego | 600.0 |
[12]:
b = Parameter(
container=m,
name="b",
domain=j,
description="demand for commodity at market j (in cases)",
records=demands.reset_index(),
)
b.records
[12]:
city | value | |
---|---|---|
0 | new-york | 325.0 |
1 | chicago | 300.0 |
2 | topeka | 275.0 |
[13]:
c = Parameter(
container=m,
name="c",
domain=[i, j],
description="cost per unit of shipment between plant i and market j",
)
The cost per unit of shipment between plant \(i\) and market \(j\) is derived from the distance between \(i\) and \(j\) and can be calculated as follows:
\(c_{ij} = \frac{90 \cdot d_{ij}}{1000}\),
where \(d_{ij}\) denotes the distance between \(i\) and \(j\).
We have two options to calculate \(c_{ij}\) and assign the data to the GAMSPy parameter:
Python assignment - calculation in Python, e.g., using Pandas and
<parameter name>.setRecords()
GAMSPy assignment - calculation in GAMSPy
Python Assignment#
[14]:
cost = freight_cost * distances / 1000
cost
[14]:
distance | ||
---|---|---|
from | to | |
seattle | new-york | 0.225 |
chicago | 0.153 | |
topeka | 0.162 | |
san-diego | new-york | 0.225 |
chicago | 0.162 | |
topeka | 0.126 |
[15]:
c.setRecords(cost.reset_index())
c.records
[15]:
from | to | value | |
---|---|---|---|
0 | seattle | new-york | 0.225 |
1 | seattle | chicago | 0.153 |
2 | seattle | topeka | 0.162 |
3 | san-diego | new-york | 0.225 |
4 | san-diego | chicago | 0.162 |
5 | san-diego | topeka | 0.126 |
GAMSPy Assignment#
For the direct assignment we need to declare a new Parameter
denoting the distances between \(i\) and \(j\).
[16]:
d = Parameter(
container=m,
name="d",
domain=[i, j],
description="distance between plant i and market j",
records=distances.reset_index(),
)
d.records
[16]:
from | to | value | |
---|---|---|---|
0 | seattle | new-york | 2.5 |
1 | seattle | chicago | 1.7 |
2 | seattle | topeka | 1.8 |
3 | san-diego | new-york | 2.5 |
4 | san-diego | chicago | 1.8 |
5 | san-diego | topeka | 1.4 |
[17]:
c[i, j] = freight_cost * d[i, j] / 1000
c.records
[17]:
i | j | value | |
---|---|---|---|
0 | seattle | new-york | 0.225 |
1 | seattle | chicago | 0.153 |
2 | seattle | topeka | 0.162 |
3 | san-diego | new-york | 0.225 |
4 | san-diego | chicago | 0.162 |
5 | san-diego | topeka | 0.126 |
Further information on the usage of parameters can be found in our parameter section of the user guide.
Variables#
GAMSPy variables are declared using Variable
. Each Variable
is assigned a name, a domain if necessary, a type, and, optionally, a description.
[18]:
x = Variable(
container=m,
name="x",
domain=[i, j],
type="Positive",
description="amount of commodity to ship from plant i to market j",
)
This statement results in the declaration of a shipment variable for each (i,j) pair.
More information on variables can be found in the variable section of our user guide.
Equations#
A GAMSPy Equation
must be declared and defined in two separate statements. The format of the declaration is the same as for other GAMSPy symbols. First comes the keyword, Equation
in this case, followed by the name, domain and text. The transportation problem has two constraints:
Supply: observe supply limit at plant \(i\): \(\sum_j x_{ij} \le a_i \: \forall i\)
Demand: satisfy demand at market \(j\): \(\sum_i x_{ij} \ge b_j \: \forall j\)
[19]:
supply = Equation(
container=m, name="supply", domain=i, description="observe supply limit at plant i"
)
demand = Equation(
container=m, name="demand", domain=j, description="satisfy demand at market j"
)
The components of an Equation
definition are: 1. The Python variable of the Equation
being defined 2. The domain (optional) 3. Domain restricting conditions (optional) 4. A =
sign 5. Left hand side expression 6. Relational operator (==
, <=
, >=
) 7. The right hand side expression.
The Equation
definition for the supply constraint of the transportation problem is implemented as follows:
[20]:
supply[i] = Sum(j, x[i, j]) <= a[i]
Using the same logic as above, we can define the demand equation as follows:
[21]:
demand[j] = Sum(i, x[i, j]) >= b[j]
More information on equations is given in the equation section of our user guide.
Objective#
The objective function of a GAMSPy Model
does not require a separate Equation
declaration. You can assign the objective expression to a Python variable or use it directly in the Model()
statement of the next section.
[22]:
obj = Sum((i, j), c[i, j] * x[i, j])
Model#
A GAMSPy Model()
consolidates constraints, an objective function, a sense (minimize, maximize, and feasibility), and a problem type. It also possesses a name and is associated with a Container
.
To define our transportation problem as a GAMSPy Model
, we assign it to a Python variable, link it to our Container
(populated with symbols and data), name it “transport”, specify the equations, set the problem type as linear program (LP), specify the sense of the objective function (Sense.MIN
), and point to the objective expression.
GAMSPy allows two alternatives to assign equations to a Model
: 1. Using a list of equations, 2. Retrieving all equations by calling m.getEquations()
.
Using a List of Equations#
Using a list of equations is especially useful if you want to define multiple GAMSPy Model
s with a subset of the equations in your Container
. For the transportation problem this can be done as follows:
[23]:
transport = Model(
m,
name="transport",
equations=[supply, demand],
problem="LP",
sense=Sense.MIN,
objective=obj,
)
Retrieving all Equations#
Using m.getEquations()
is especially convenient if you want to include all equations of your Container
to be associated with your model. For the transportation problem this can be done as follows:
[24]:
transport_2 = Model(
m,
name="transport2",
equations=m.getEquations(),
problem="LP",
sense=Sense.MIN,
objective=obj,
)
More information on the usage of a GAMSPy Model
can be found in the model section of our user guide.
Solve#
Upon defining the GAMSPy Model
, it’s ready for being solved. The solve()
statement triggers the generation of the specific model instance, creates suitable data structures for the solver, and invokes the solver. To view solver output in the console, the sys
library can be used, passing the output=sys.stdout
attribute to transport.solve()
.
[25]:
import sys
transport.solve(output=sys.stdout)
--- Job _gams_py_gjo1.gms Start 11/13/23 14:08:11 45.1.0 88bbff72 WEX-WEI x86 64bit/MS Windows
--- Applying:
C:\Users\jbroi\anaconda3\envs\gamspy\Lib\site-packages\gamspy_base\gmsprmNT.txt
C:\Users\jbroi\Documents\GAMS\gamsconfig.yaml
--- GAMS Parameters defined
LP CPLEX
MIP CPLEX
RMIP CPLEX
NLP CONOPT
MCP PATH
MPEC NLPEC
RMPEC CONVERT
CNS CONOPT
DNLP CONOPT
RMINLP CONOPT
MINLP SBB
QCP CONOPT
MIQCP SBB
RMIQCP CONOPT
EMP CONVERT
Restart C:\Users\jbroi\AppData\Local\Temp\tmp3r7ggl2s\_gams_py_gcp0.g00
Input C:\Users\jbroi\AppData\Local\Temp\tmp3r7ggl2s\_gams_py_gjo1.gms
Output C:\Users\jbroi\AppData\Local\Temp\tmp3r7ggl2s\_gams_py_gjo1.lst
Save C:\Users\jbroi\AppData\Local\Temp\tmp3r7ggl2s\_gams_py_gcp1.g00
ScrDir C:\Users\jbroi\AppData\Local\Temp\tmp3r7ggl2s\225a\
SysDir C:\Users\jbroi\anaconda3\envs\gamspy\Lib\site-packages\gamspy_base\
CurDir C:\Users\jbroi\AppData\Local\Temp\tmp3r7ggl2s\
LogOption 3
LogFile C:\Users\jbroi\AppData\Local\Temp\tmp3r7ggl2s\_gams_py_gjo1.log
GDX C:\Users\jbroi\AppData\Local\Temp\tmp3r7ggl2s\_gams_py_gcp0.gdx
Licensee: GAMS Demo, for EULA and demo limitations see G230706/0001CB-GEN
https://www.gams.com/latest/docs/UG%5FLicense.html DC0000
C:\Users\jbroi\Documents\GAMS\gamslice.txt
Demo license for demonstration and instructional purposes only
Processor information: 1 socket(s), 12 core(s), and 16 thread(s) available
GAMS 45.1.0 Copyright (C) 1987-2023 GAMS Development. All rights reserved
--- Starting continued compilation
--- Workfile was generated under GAMS version WEX451-451
--- _gams_py_gjo1.gms(2) 3 Mb
--- GDXin=C:\Users\jbroi\AppData\Local\Temp\tmp3r7ggl2s\_gams_py_gcp1.gdx
--- GDX File ($gdxIn) C:\Users\jbroi\AppData\Local\Temp\tmp3r7ggl2s\_gams_py_gcp1.gdx
--- _gams_py_gjo1.gms(138) 3 Mb
--- Starting execution: elapsed 0:00:00.067
--- Generating LP model transport
--- _gams_py_gjo1.gms(123) 4 Mb
--- 6 rows 7 columns 19 non-zeroes
--- Range statistics (absolute non-zero finite values)
--- RHS [min, max] : [ 2.750E+02, 6.000E+02] - Zero values observed as well
--- Bound [min, max] : [ NA, NA] - Zero values observed as well
--- Matrix [min, max] : [ 1.260E-01, 1.000E+00]
--- Executing CPLEX (Solvelink=2): elapsed 0:00:00.087
IBM ILOG CPLEX 45.1.0 88bbff72 Oct 14, 2023 WEI x86 64bit/MS Window
*** This solver runs with a demo license. No commercial use.
--- GMO setup time: 0.00s
--- GMO memory 0.50 Mb (peak 0.50 Mb)
--- Dictionary memory 0.00 Mb
--- Cplex 22.1.1.0 link memory 0.00 Mb (peak 0.00 Mb)
--- Starting Cplex
Version identifier: 22.1.1.0 | 2022-11-27 | 9160aff4d
CPXPARAM_Advance 0
CPXPARAM_Simplex_Display 2
CPXPARAM_Threads 1
CPXPARAM_MIP_Display 4
CPXPARAM_MIP_Pool_Capacity 0
CPXPARAM_MIP_Tolerances_AbsMIPGap 0
Tried aggregator 1 time.
LP Presolve eliminated 0 rows and 1 columns.
Reduced LP has 5 rows, 6 columns, and 12 nonzeros.
Presolve time = 0.00 sec. (0.00 ticks)
Iteration Dual Objective In Variable Out Variable
1 73.125000 x(seattle,new-york) demand(new-york) slack
2 119.025000 x(seattle,chicago) demand(chicago) slack
3 153.675000 x(san-diego,topeka) demand(topeka) slack
4 153.675000 x(san-diego,new-york) supply(seattle) slack
--- LP status (1): optimal.
--- Cplex Time: 0.00sec (det. 0.01 ticks)
Optimal solution found
Objective: 153.675000
--- Reading solution for model transport
--- Executing after solve: elapsed 0:00:00.251
--- _gams_py_gjo1.gms(153) 4 Mb
--- GDX File C:\Users\jbroi\AppData\Local\Temp\tmp3r7ggl2s\_gams_py_gcp0.gdx
*** Status: Normal completion
--- Job _gams_py_gjo1.gms Stop 11/13/23 14:08:11 elapsed 0:00:00.251
Retrieving Results#
Variable Values#
The values of the variables in the solution can be retrieved using using <variable name>.records
. The level specifies the shipment quantities \(x_{ij}\). Other variable attributes are the marginal values, lower and upper bounds, and the variable’s scaling factor.
[26]:
x.records.set_index(["i", "j"])
[26]:
level | marginal | lower | upper | scale | ||
---|---|---|---|---|---|---|
i | j | |||||
seattle | new-york | 50.0 | 0.000 | 0.0 | inf | 1.0 |
chicago | 300.0 | 0.000 | 0.0 | inf | 1.0 | |
topeka | 0.0 | 0.036 | 0.0 | inf | 1.0 | |
san-diego | new-york | 275.0 | 0.000 | 0.0 | inf | 1.0 |
chicago | 0.0 | 0.009 | 0.0 | inf | 1.0 | |
topeka | 275.0 | 0.000 | 0.0 | inf | 1.0 |
Objective Value#
The optimal objective function value can be accessed by <model name>.objective_value
.
[27]:
transport.objective_value
[27]:
153.675