Tutorial

The goal of this tutorial is to provide a compact overview of the basic functionality of the GAMS Matlab API. It allows the user to start immediately working with the API by providing a set of small examples based on the well-known transportation problem. These examples introduce several API features step by step.

Getting started

The object oriented GAMS Matlab API is built on top of the GAMS Java API and provides convenient access to GAMS from within Matlab (2017b or later) and Octave (5.2 or later). Examples using the API are located in apifiles/Matlab/examples and apifiles/Matlab/examples_octave for Matlab or Octave, respectively. The examples slightly differ for Octave since it does not support the complete Matlab syntax and functionalities - most importantly to note, enumeration classes (see also the note below). The object oriented API is located in apifiles/Matlab/api.

For making your Matlab installation aware of the packages provided by GAMS, you need to do the following two steps. First, add the GAMS Java API to the Matlab java class path. This can be done statically or dynamically. The latter is less efficient but more convenient for testing.

  • static: Locate the javaclasspath.txt and add the path [PathToGAMS]/apifiles/Java/api/GAMSJavaAPI.jar. You can find the location with cd(prefdir) in Matlab or which javaclasspath in Octave. Finally, restart Matlab or Octave.
  • dynamic: Call the function javaaddpath("[PathToGAMS]/apifiles/Java/api/GAMSJavaAPI.jar").

Second, add the GAMS Matlab API to the Matlab path by calling addpath("[PathToGAMS]/apifiles/Matlab/api").

Note
Octave does not support enumeration classes (yet). In order to use the GAMS enumerations, please use the alternative integer or string value. For example, instead of using GAMS.DebugLevel.SHOW_LOG, use 2, see DebugLevel. To tell the GAMS Matlab API to return the alternative values instead of instances of enumeration classes, set the environment variable GAMS_MATLAB_ENUM_ALT to true, i.e., setenv("GAMS_MATLAB_ENUM_ALT", "true");.

You can test your setup by creating a GAMSWorkspaceInfo:

>> wsinfo = GAMS.GAMSWorkspaceInfo()
wsinfo =
GAMSWorkspaceInfo with properties:
debugLevel: OFF
systemDirectory: ''
workingDirectory: ''
Note
In Matlab you can import the GAMS package by import GAMS.*. Then, you don't need to call the GAMS classes with the preceding GAMS..

Important Classes of the API

This section provides a quick overview of some fundamental classes of the GAMS package. Their usage is demonstrated by an extensive set of examples. All GAMS Matlab API classes are contained within one single package GAMS. It provides objects to interact with the General Algebraic Modeling System (GAMS). Objects in this package allow convenient exchange of input data and model results (GAMSDatabase) and help to create and run GAMS models (GAMSJob), that can be customized by GAMS options (GAMSOptions). Furthermore, it introduces a way to solve a sequence of closely related models in the most efficient way (GAMSModelInstance).

Other classes are GAMSWorkspace, GAMSOptions and GAMSSymbol.

How to use the API

In the GAMS system directory there are some examples provided that illustrate the usage of the Matlab API. [PathToGAMS]/apifiles/Matlab/examples contains multiple examples dealing - among others - with the well-known transportation problem. In further course of this tutorial we discuss these examples step by step and introduce new elements of the API in detail.

We recommend to open the aforementioned files to gain a complete overview of the examples. Down below we explain the examples with the help of selected code snippets.

How to choose the GAMS system (transport1)

By default the GAMS system is determined automatically. In case of having multiple GAMS systems on your machine, the desired system can be specified via an additional argument when the workspace is created. When running the examples, we can provide an additional command line argument in order to define the GAMS system directory that should be used. By executing transport1 with C:/GAMS/win64/43.3 we use the 64-bit version of GAMS 43.3 to run transport1 even if our default GAMS system might be a different one. This is managed by the following code:

wsInfo = GAMS.GAMSWorkspaceInfo();
if nargin > 0
wsInfo.systemDirectory = varargin{1};
end
ws = GAMS.GAMSWorkspace(wsInfo);
Note
The API can detect GAMS automatically from the PATH environment variable. Please note that this is not the MATLABPATH. You can inspect the PATH with getenv("PATH").

How to export data to GDX (transport_gdx)

Although the Object-oriented Matlab API offers much more than exchanging data between Matlab and GDX, a common use case is the export and import of GDX files. The central class for this purpose is GAMSDatabase. We assume that the data to be exported is available in Matlab data structures.

plants = {'Seattle', 'San-Diego'};
markets = {'New-York', 'Chicago', 'Topeka'};
capacity = containers.Map();
capacity('Seattle') = 350;
capacity('San-Diego') = 600;
demand = containers.Map();
demand('New-York') = 325;
demand('Chicago') = 300;
demand('Topeka') = 275;
distance = containers.Map();
distance('Seattle.New-York') = 2.5;
distance('Seattle.Chicago') = 1.7;
distance('Seattle.Topeka') = 1.8;
distance('San-Diego.New-York') = 2.5;
distance('San-Diego.Chicago') = 1.8;
distance('San-Diego.Topeka') = 1.4;

Different type of GAMS symbols are represented using different Matlab data structures. The data for the GAMS sets is represented using a cell of strings (e.g. plants and markets). On the other hand, GAMS parameters are represented by a containers.Map (e.g. capacity and demand). Note that the representation of the two dimensional parameter distance uses a dot notation for storing the keys. The choice of data structures can also be different, but the used structures in this example fit well for representing GAMS data with Matlab data structures.

A new GAMSDatabase instance can be created using GAMSWorkspace.addDatabase.

db = ws.addDatabase();

We start adding GAMS sets using the method GAMSDatabase.addSet which takes the name and the dimension as arguments. The third argument is an optional explanatory text. A for-loop iterates through plants and adds new records to the recently created GAMSSet instance i using GAMSSet.addRecord.

i = db.addSet('i', 1, 'canning plants');
for p = plants
i.addRecord(p{1});
end

GAMSParameter instances can be added by using the method GAMSDatabase.addParameter. In this example we use the overloaded method which takes a list of GAMSSet instances instead of the dimension for creating a parameter with domain information.

a = db.addParameter('a', 'capacity of plant i in cases', i);
for p = plants
a.addRecord(p{1}).value = capacity(p{1});
end

As soon as all data is prepared in the GAMSDatabase, the method GAMSDatabase.export can be used to create a GDX file.

db.export('data.gdx');

How to import data from GDX (transport_gdx)

Data can be imported from a GDX file using GAMSWorkspace.addDatabaseFromGDX. The method takes a path to a GDX file and creates a GAMSDatabase instance.

gdxdb = ws.addDatabaseFromGDX('data.gdx');

Reading the data from the GAMSSet i into a cell of strings can be done as follows:

gdxPlantsRecords = gdxdb.getSet('i').records;
gdxPlants = cell(size(gdxPlantsRecords));
for i = 1:numel(gdxPlants)
gdxPlants{i} = gdxPlantsRecords{i}.key(1);
end

i is retrieved by calling GAMSDatabase.getSet on gdxdb. The returned GAMSSet object has an attribute records with an cell array of GAMSSSetRecords. set. Each record can be asked for its keys.

You can do the same for GAMSParameter. Instead of creating a cell, we want to have the data in the form of a containers.Map. GAMSParameterRecord can not only be asked for its keys, but also for its value. The following code snippet shows how to read the one dimensional parameter a into a map.

gdxCapacity = containers.Map();
for rec = gdxdb.getParameter('a').records
gdxCapacity(rec{1}.key(1)) = rec{1}.value;
end

For a key of multi dimensional symbol, we choose a dot based concatenation of keys.

gdxDistance = containers.Map();
for rec = gdxdb.getParameter('d').records
gdxCapacity([rec{1}.key(1), '.', rec{1}.key(2)]) = rec{1}.value;
end

Scalar can be read into a variable of type double by accessing the value of the first and only record.

gdxFreight = gdxdb.getParameter('f').record.value;

How to run a GAMSJob from file (transport1)

At first we create our workspace using GAMSWorkspace ws = GAMS.GAMSWorkspace();. Afterwards, we can create a GAMSJob t1 using the addJobFromGamsLib method and run it.

Apparently you can create a GAMSJob with any other gms file you might have created on your own as long as it is located in the current working directory. Then the GAMSJob t1 can be defined using the GAMSJob.addJobFromFile method.

% create GAMSWorkspace "ws" with default working directory
ws = GAMS.GAMSWorkspace();
% create GAMSJob "t1" from "trnsport" model in GAMS Model Libraries
t1 = ws.addJobFromGamsLib('trnsport');
% run GAMSJob "t1"
t1.run();

How to retrieve a solution from an output database (transport1)

The following lines create the solution output and illustrate the usage of the GAMSJob.outDB property to get access to the GAMSDatabase created by the run method. To retrieve the content of variable x we use the getVariable method and the GAMSVariableRecord class.

% retrieve GAMSVariable "x" from GAMSJob's output databases
fprintf('Ran with Default:\n');
for x = t1.outDB.getVariable('x').records
fprintf('x(%s,%s): level=%g marginal=%g\n', x{1}.keys{:}, x{1}.level, x{1}.marginal);
end

How to specify the solver using GAMSOptions (transport1)

The solver can be specified via the GAMSOptions class and the GAMSWorkspace.addOptions method. The GAMSOptions.setAllModelTypes property sets xpress as default solver for all model types which the solver can handle. Then we run our GAMSJob t1 with the new GAMSOptions.

% create GAMSOptions 'opt1'
opt1 = ws.addOptions();
% set all model types of 'opt1' for 'xpress'
opt1.setAllModelTypes('xpress');
% run GAMSJob 't1' with GAMSOptions 'opt1'
t1.run(opt1);

How to run a job with a solver option file and capture its log output (transport1)

At first we create the file xpress.opt with content algorithm=barrier which will be used as solver option file and is stored in the current working directory. Afterward we use a GAMSOptions just like in the preceding example and GAMSOptions.optFile property to 1 to tell the solver to look for a solver option file. We specify the argument output in order to stream the log of the GAMSJob into the file transport1_xpress.log. When the output argument is omitted then the log will be written to standard output.

% write file 'xpress.opt' under GAMSWorkspace's working directory
fid = fopen([ws.workingDirectory, GAMS.GAMSGlobals.FILE_SEPARATOR, 'xpress.opt'], 'w');
fprintf(fid, 'algorithm=barrier');
fclose(fid);
% create GAMSOptions 'opt2'
opt2 = ws.addOptions();
% set all model types of 'opt2' for 'xpress'
opt2.setAllModelTypes('xpress');
% for 'opt2', use 'xpress.opt' as solver's option file
opt2.optFile = 1;
% run GAMSJob 't2' with GAMSOptions 'opt2' and capture log into 'transport1_xpress.log'.
t1.run(opt2, [ws.workingDirectory, GAMS.GAMSGlobals.FILE_SEPARATOR, 'transport1_xpress.log']);

How to use include files (transport2)

In this example, as in many succeeding, the data text and the model text are separated into two different strings. Note that these strings data and model are using GAMS syntax.

At first we write an include file tdata.gms that contains the data but not the model text:

% write 'data' into file 'tdata.gms' under GAMSWorkspace's working directory
fid = fopen([ws.workingDirectory, GAMS.GAMSGlobals.FILE_SEPARATOR, 'tdata.gms'], 'w');
fprintf(fid, data);
fclose(fid);

Afterwards we create a GAMSJob using the GAMSWorkspace.addJobFromString method. GAMSOptions.defines is used like the the 'double dash' GAMS parameters, i.e. it corresponds to --incname=tdata on the command line where incname is used as name for the include file in the model string.

% create GAMSJob 't2' from the 'model' string variable
t2 = ws.addJobFromString(model);
% create GAMSOption 'opt' and define 'incname' as 'tdata'
opt = ws.addOptions();
opt.defines('incname', 'tdata');
% run GAMSJob 't2' with GAMSOptions 'opt'
t2.run(opt);

The string model contains the following lines to read in the data.

$if not set incname $abort 'no include file name for data file provided'
$include %incname%

How to set a non-default working directory (transport3)

At first we create a new directory. Once this is done we can use this directory when creating the GAMSWorkspace and make it the working directory.

% create a directory
workingDirectory = fullfile(pwd, 'transport3');
mkdir(workingDirectory);
% create a workspace
wsInfo = GAMS.GAMSWorkspaceInfo();
wsInfo.workingDirectory = workingDirectory;
ws = GAMS.GAMSWorkspace(wsInfo);

How to read data from string and export to GDX (transport3)

We read the data from the string data. Note that this contains no model but only data definition in GAMS syntax. By running the corresponding GAMSJob a GAMSDatabase is created that is available via the GAMSJob.outDB property. We can use the GAMSDatabase.export method to write the content of this database to a gdx file tdata.gdx.

% create and run a job from a data file, then explicitly export to a GDX file
t3 = ws.addJobFromString(data);
t3.run();
t3.outDB.export([ws.workingDirectory, GAMS.GAMSGlobals.FILE_SEPARATOR, 'tdata.gdx']);

How to run a job using data from GDX (transport3)

This works quite similar to the usage of an include file explained in transport2 - How to use include files (transport2) .

% run a job using an instance of GAMSOptions that defines the data include file
t3 = ws.addJobFromString(model);
opt = ws.addOptions();
opt.defines('gdxincname', 'tdata');
t3.run(opt);

Note that there are some minor changes in the model due to the usage of a gdx instead of an include file.

$if not set gdxincname $abort 'no include file name for data file provided'
$gdxin %gdxincname%
$load i j a b d f
$gdxin
dict d
GamsSet j
GamsWorkspace b
GamsWorkspace i
GamsWorkspace a
GamsWorkspace f

How to run a job using implicit database communication (transport3)

This example does basically the same as the two preceding examples together. We create two GAMSJobs t3a and t3b where the first one contains only the data and the second one contains only the model without data. After running t3a the corresponding outDB can be read in directly just like a gdx file. Note that the database needs to be passed to the GAMSJob.run method as additional argument.

t3a = ws.addJobFromString(data);
t3b = ws.addJobFromString(model);
opt = ws.addOptions();
t3a.run();
opt.defines('gdxincname', t3a.outDB.name);
t3b.run(opt, t3a.outDB);

How to define data using Matlab data structures (transport4)

We use cell arrays and containers.Map to define Matlab data structures that correspond to the sets, parameters and tables used for the data definition in GAMS.

plants = {'Seattle', 'San-Diego'};
markets = {'New-York', 'Chicago', 'Topeka'};
capacity = containers.Map();
capacity('Seattle') = 350;
capacity('San-Diego') = 600;
demand = containers.Map();
demand('New-York') = 325;
demand('Chicago') = 300;
demand('Topeka') = 275;
distance = containers.Map();
distance('Seattle.New-York') = 2.5;
distance('Seattle.Chicago') = 1.7;
distance('Seattle.Topeka') = 1.8;
distance('San-Diego.New-York') = 2.5;
distance('San-Diego.Chicago') = 1.8;
distance('San-Diego.Topeka') = 1.4;

How to prepare a GAMSDatabase from Matlab data structures (transport4)

At first we create an empty GAMSDatabase db using the GAMSWorkspace.addDatabase method. Afterwards we prepare the database. To add a set to the database we use the GAMSSet class and the GAMSDatabase.addSet method with arguments describing the identifier, dimension and explanatory text. To add the records to the database we iterate over the elements of our Matlab data structure and add them by using the GAMSSet.addRecord method.

For parameters the procedure is pretty much the same. Note that the table that specifies the distances in GAMS can be treated as parameter with dimension 2 and that scalars can be treated as parameter with dimension 0.

The GAMSJob can be run like explained in the preceding example about implicit database communication.

db = ws.addDatabase();
i = db.addSet('i', 1, 'canning plants');
for p = plants
i.addRecord(p{1});
end
j = db.addSet('j', 1, 'markets');
for m = markets
j.addRecord(m{1});
end
a = db.addParameter('a', 'capacity of plant i in cases', i);
for p = plants
a.addRecord(p{1}).value = capacity(p{1});
end
b = db.addParameter('b', 'demand at market j in cases', j);
for m = markets
b.addRecord(m{1}).value = demand(m{1});
end
d = db.addParameter('d', 'distance in thousands of miles', i, j);
for p = plants
for m = markets
d.addRecord(p{1}, m{1}).value = distance([p{1}, '.', m{1}]);
end
end
f = db.addParameter('f', 'freight in dollars per case per thousand miles');
f.addRecord().value = 90;
% create and run a job from the model and read gdx include file from the database
t4 = ws.addJobFromString(model);
opt = ws.addOptions();
opt.defines('gdxincname', db.name);
t4.run(opt, db);

How to initialize a GAMSCheckpoint by running a GAMSJob (transport5)

The following lines of code conduct several operations. While the first line simply creates a GAMSCheckpoint, the second one uses the GAMSWorkspace.addJobFromString method to create a GAMSJob containing the model text and data but no solve statement. Afterwards the run method gets the GAMSCheckpoint as argument. That means the GAMSCheckpoint cp captures the state of the GAMSJob.

cp = ws.addCheckpoint();
t5 = ws.addJobFromString(model);
t5.run(cp);

How to initialize a GAMSJob from a GAMSCheckpoint (transport5)

Note that the string returned from function model contains the entire model and data definition plus an additional demand multiplier and scalars for model and solve status but no solve statement:

Scalar bmult demand multiplier /1/;
...
demand(j) .. sum(i, x(i,j)) =g= bmult*b(j) ;
...
Scalar ms 'model status', ss 'solve status';
GamsWorkspace demand
GamsWorkspace bmult
GamsWorkspace x

In transport5 we create a list with eight different values for this demand multiplier.

bmultlist = [0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3];

For each entry of that list we create a GAMSJob t5 using the GAMSWorkspace.addJobFromString method. Besides another string which resets the demand multiplier bmult, specifies the solve statement and assigns values to the scalars ms and ss we pass the checkpoint cp as additional argument. This results in a GAMSJob combined from the checkpoint plus the content provided by the string. We run the GAMSJob and echo some interesting data from the outDB using the GAMSDatabase.getParameter and GAMSDatabase.getVariable methods and the GAMSSymbol.record attribute plus the GAMSParameterRecord.value and the GAMSVariableRecord.level properties.

for i = 1:numel(bmultlist)
job = sprintf('bmult=%f; solve transport min z use lp; ms=transport.modelstat; ss=transport.solvestat;', bmultlist(i));
t5 = ws.addJobFromString(job, cp);
t5.run();
fprintf('Scenario bmult=%f:\n', bmultlist(i));
fprintf(' Modelstatus: %s\n', GAMS.ModelStat.lookup(t5.outDB.getParameter('ms').record.value));
fprintf(' Solvestatus: %s\n', GAMS.SolveStat.lookup(t5.outDB.getParameter('ss').record.value));
fprintf(' Obj: %f\n', t5.outDB.getVariable('z').record.level);
end
Note
Some of the demand multipliers cause infeasibility. Nevertheless, GAMS keeps the incumbent objective function value. Therefore the model status and the solve status provide important information for a correct solution interpretation.

How to create a GAMSModelInstance from a GAMSCheckpoint (transport7)

In transport7 the usage of Matlab::GAMS::GAMSModelInstance is demonstrated.

At first checkpoint cp is created as in the preceding examples. Then we create the GAMSModelInstance mi using the GAMSCheckpoint.addModelInstance method. Note that the GAMSJob again contains no solve statement and the demand multiplier is already included with default value 1.

cp = ws.addCheckpoint();
% initialize a checkpoint by running a job
t7 = ws.addJobFromString(model);
t7.run(cp);
% create a ModelInstance and solve it multiple times with different scalar bmult
mi = cp.addModelInstance();

How to modify a parameter of a GAMSModelInstance using GAMSModifier (transport7)

A GAMSModelInstance uses a syncDB to maintain the data. We define bmult as GAMSParameter using the GAMSParameter method and specify cplex as solver. Afterwards the GAMSModelInstance is instantiated with 3 arguments, the solve statement, GAMSOptions opt and GAMSModifier bmult. The GAMSModifier means that bmult is modifiable while all other parameters, variables and equations of ModelInstance mi stay unchanged. We use the GAMSParameter.addRecord method and the value property to assign a value to bmult. That value can be varied afterwards using the GAMSSymbol.record property to reproduce our well-known example with different demand multipliers.

bmult = mi.syncDB.addParameter('bmult', 'demand multiplier');
opt = ws.addOptions();
opt.setAllModelTypes('cplex');
% instantiate the ModelInstance and pass a model definition and Modifier to declare bmult mutable
mi.instantiate('transport use lp min z', opt, GAMS.GAMSModifier(bmult));
bmult.addRecord().value = 1.0;
bmultlist = [0.6, 0.7 , 0.8, 0.9, 1.0, 1.1, 1.2, 1.3];
for i = 1:numel(bmultlist)
bmult.record.value = bmultlist(i);
mi.solve();
fprintf('Scenario bmult=%f:\n', bmultlist(i));
fprintf(' Modelstatus: %s\n', mi.modelStatus);
fprintf(' Solvestatus: %s\n', mi.solveStatus);
fprintf(' Obj: %f\n', mi.syncDB.getVariable('z').record.level);
end

How to modify a variable of a GAMSModelInstance using GAMSModifier (transport7)

We create a GAMSModelInstance just like in the next to last example. We define x as GAMSVariable and its upper bound as GAMSParameter xup. At the following instantiate method GAMSModifier has 3 arguments. The first one says that x is modifiable, the second determines which part of the variable (lower bound, upper bound or level) can be modified and the third specifies the GAMSParameter that holds the new value.

In the following loops we set the upper bound of one link of the network to zero, which means that no transportation between the corresponding plant and market is possible, and solve the modified transportation problem.

mi = cp.addModelInstance();
x = mi.syncDB.addVariable('x', 2, GAMS.VarType.POSITIVE, '');
xup = mi.syncDB.addParameter('xup', 2, 'upper bound on x');
% instantiate the ModelInstance and pass a model definition and Modifier to declare upper bound of X mutable
mi.instantiate('transport use lp min z', GAMS.GAMSModifier(x, GAMS.UpdateAction.UPPER, xup));
for i = t7.outDB.getSet('i').records
for j = t7.outDB.getSet('j').records
xup.clear();
keys = {i{1}.key(1), j{1}.key(1)};
xup.addRecord(keys).value = 0;
mi.solve();
fprintf('Scenario link blocked: %s - %s\n', keys{:});
fprintf(' Modelstatus: %s\n', mi.modelStatus);
fprintf(' Solvestatus: %s\n', mi.solveStatus);
fprintf(' Obj: %f\n', mi.syncDB.getVariable('z').record.level);
end
end

How to create and use a save/restart file (transport11)

In transport11 we demonstrate how to create and use a save/restart file. Usually such a file should be supplied by an application provider but in this example we create one for demonstration purpose. Note that the restart is launched from a GAMSCheckpoint. From the main function we call the function create_save_restart giving it the current workspace settings and the desired file name as arguments.

create_save_restart(ws.workingDirectory, ws.systemDirectory, ws.debugLevel, 'tbase');

In function create_save_restart we create a workspace with the given workspace settings. Then we create a GAMSJob from a string. Note that the string given via basemodel contains the basic definitions of sets without giving them a content (that is what $onempty is used for). Afterwards we specify a GAMSOptions to only compile the job but do not execute it. Then we create a checkpoint cp that is initialized by the following run of the GAMSJob and stored in the file given as argument to the function, in our case tbase. This becomes possible because the addCheckpoint method accepts identifiers as well as file names as argument.

ws = GAMS.GAMSWorkspace(workdir, systemdir, debuglevel);
j1 = ws.addJobFromString(basemodel);
opt = ws.addOptions();
opt.action = GAMS.Action.CompileOnly;
cp = ws.addCheckpoint('tbase');
j1.run(opt, cp);
opt.dispose();
j1.outDB.dispose();

So what you should keep in mind before we return to further explanations of the main function is, that the file tbase is now in the current working directory and contains a checkpoint. Now in the main function we define some data using Matlab data structures as we already did in transport4 before we create the GAMSWorkspace and a GAMSDatabase.

ws = GAMS.GAMSWorkspace(wsInfo);
db = ws.addDatabase();

Afterwards we set up the GAMSDatabase like we already did in transport4. Once this is done we run a GAMSJob using this data plus the checkpoint stored in file tbase.

cpBase = ws.addCheckpoint('tbase');
opt = ws.addOptions();
t11 = ws.addJobFromString(model, cpBase);
opt.defines('gdxincname', db.name);
opt.setAllModelTypes('xpress');
t11.run(opt, db);

Note that the string from which we create job t11 is different to the one used to prepare the checkpoint stored in tbase and is only responsible for reading in the data from the GAMSDatabase correctly. The entire model definition is delivered by the checkpoint cpBase which is equal to the one we saved in tbase.