Executing GAMS from other Environments

Some General Comments

Nowadays the objected-oriented APIs are the most efficient and elegant way to interact with the GAMS system. They allow the effective communication of data, and do parameterized runs of GAMS. This whole chapter describes a much more basic interaction of GAMS through calling the GAMS executable from different environments. This still can be useful, e.g. if no object-oriented API is available for the particular target language (e.g. VBA).

While the principle of calling the GAMS executable holds for all operating systems, this chapter often focuses on the Windows platform.

One of the interesting problems one faces when spawning GAMS.EXE in a Windows environment is multi-threading. If one does not take precautions, a call to Shell (VB function) or CreateProcess (Win32 API function) causes GAMS to run asynchronously: the function will return while GAMS is still running. In order to read the results one would need to wait until the GAMS job has finished. The machinery for this requires some Windows trickery, and for Visual Basic version 6 and Delphi version 4 we have implemented some small examples that illustrate how this can be done.

Another issue that needs to be addressed is that GAMS needs a place to put its scratch files. By default this is the current directory, a concept that is not always clear in a windowing environment. A good way of dealing with his is to set both the current drive and the current directory before running the GAMS job. It should be noted that GAMS needs write permission there. In the examples we use the Windows TEMP directory for this, but in a real application you may want to use a designated directory. The Windows TEMP directory is found by calling the API function GetTempPath.

The GAMS architecture

GAMS itself is a console mode application. In fact it is not a single program, but a driver program (GAMS.EXE on Windows, otherwise GAMS) that executes in turn the GAMS language compiler (GAMSCMEX) or one of the solvers. For a model with a single solve statement, GAMS.EXE will first call GAMSCMEX to compile the GAMS model. Then GAMSCMEX will start executing the internal code that was generated by the compiler. As soon as it hits the instructions belonging to a SOLVE statement it will generate model instance, and GAMSCMEX will exit. Then GAMS.EXE will spawn a solver capable of solving the model. As soon as the solver is finished, GAMS.EXE will execute GAMSCMEX again so it can read the solution and can continue with executing instructions.

Spawning GAMS from VBA

Visual Basic for Applications (VBA) is a programming language that is built into most Microsoft Office applications, e.g. Excel and Access. A VBA program may include modules, which can be imported from files, i.e. in the VBA editor you can choose menu “File”→“Import File”. The GAMS distribution includes some VBA modules, which can be found from:

  <GAMS System Directory>\apifiles\VBA\api, e.g. "C:\GAMS\win64\24.5\apifiles\VBA\api"
Attention
  • In order to avoid issues, it's recommended to use the latest version of the modules , i.e. the modules found in the latest GAMS release.

For example, the following modules can be found:

  • gamsglobals.bas: Global constants that are used in other modules
  • gdxvba.bas: GAMS Data Exchange Object
  • idxvba.bas: GAMS IDX Object
  • optvba.bas: GAMS Option Object

    For more information, see Expert-Level APIs. VBA programs that use the API can be found from:

      <GAMS System Directory>\apifiles\VBA, e.g. "C:\GAMS\win64\24.5\apifiles\VBA"
    

    The models can also be retrieved via GAMS Studio -> GAMS --> Model Library Explorer -> GAMS Data Utilities Models.

Spawning GAMS from Excel

Calling GAMS out of Excel requires some more work than just Data Exchange with Microsoft Excel. The application calling GAMS out of Excel has to:

  1. Locate the GAMS system directory and adjust the system path accordingly.
  2. Copy the GAMS model into a temporary directory (by default the temporary directory of Windows)
  3. Extract the model data from the spreadsheet into a GAMS readable format (gdx)
  4. Execute GAMS (solve the model, write the slution back to gdx file)
  5. Import the model results back from the gdx files into the spread sheet
  6. Update the spreadsheet (graphics, tables)

Using VisualBasic for Applications (VBA) this can be implemented with a few lines of code:

Sub solve()
Dim WorkDir As String
WorkDir = TempDir()
Call ClearSolution
If (Not AddGAMSPath()) Then ' needed to find gdxcclib64.dll and gams.exe
Exit Sub
End If
Call ExportGDXFile(WorkDir)
Call WriteGAMSModel(WorkDir & "portfolio.gms")
Call RunGAMS(WorkDir & "portfolio.gms", WorkDir)
Call ReadListing(WorkDir & "portfolio.lst")
Call ImportGDXFile(WorkDir)
End Sub

For further details, please inspecte the VBA part of the examples below.

A Simple Example

This very simple example shows how GAMS can be invoked from an Excel spreadsheet. The "example spreadsheet" has a button, which will cause GAMS to run the trnsport.gms model stored in c:/tmp. There is no data exchange.

A more complete application will write an include file for a GAMS model, and will import a comma delimited file with results when the run is finished. An example of such a complete application is described in http://www.gams.com/mccarl/excelgams.pdf.

Sudoku Example

This spreadsheet is a complete example that uses GDX files to exchange information solves a 25x25 Sudoku problem using CPLEX. It comes with the GAMS distribution in apifiles/VBA/sudoku.xlsm. You will need a GAMS/CPLEX license to be able to run the spreadsheet. The MIP model solves very easily: the solution is found in the presolve phase.

Note: This spreadsheet requires distribution 22.6 or younger to work properly.

Problem

Solution

Efficient frontier example

This example solves a series of NLPs to create an efficient frontier of a portfolio optimization problem. It comes with the GAMS distribution in apifiles/VBA/portfolio.xlsm.

Note: This spreadsheet requires distribution 22.6 or younger to work properly.

Spawning GAMS from C

The example below shows an absolute minimal version of calling GAMS from a C program:

#include <stdio.h>
int main(int argc, char** argv)
{
system("gams trnsport lo=2");
return 0;
}

As can be seen the GAMS executable gams.exe is called via a simple invocation of system(). The extra command line parameter lo=2 indicates that the log file is not written to the screen (this is the default) but to a file model.log.

If we want to generate some data from the application program (e.g. parameter DEMAND), we can use an include file which is written by the application. Of the way back results can be read from a PUT file. The GAMS file can now look like:

Sets
       i   canning plants   / seattle, san-diego /
       j   markets          / new-york, chicago, topeka / ;

  Parameters

       a(i)  capacity of plant i in cases
         /    seattle     350
              san-diego   600  /

       b(j)  demand at market j in cases
         /
$include demand.inc
         / ;

  Table d(i,j)  distance in thousands of miles
                    new-york       chicago      topeka
      seattle          2.5           1.7          1.8
      san-diego        2.5           1.8          1.4  ;

  Scalar f  freight in dollars per case per thousand miles  /90/ ;

  Parameter c(i,j)  transport cost in thousands of dollars per case ;

            c(i,j) = f * d(i,j) / 1000 ;

  Variables
       x(i,j)  shipment quantities in cases
       z       total transportation costs in thousands of dollars ;

  Positive Variable x ;

  Equations
       cost        define objective function
       supply(i)   observe supply limit at plant i
       demand(j)   satisfy demand at market j ;

  cost ..        z  =e=  sum((i,j), c(i,j)*x(i,j)) ;

  supply(i) ..   sum(j, x(i,j))  =l=  a(i) ;

  demand(j) ..   sum(i, x(i,j))  =g=  b(j) ;

  Model transport /all/ ;

  Solve transport using lp minimizing z ;

  Display x.l, x.m ;

file fout /results.txt/;
put fout;
loop((i,j),
   put x.l(i,j):17:5/;
);
putclose;

The corresponding C program could look like:

//
// example of running a GAMS model
//
#include <stdio.h>
// number of supply and demand locations
#define M 2
#define N 3
char* dist[M] = {"seattle", "san-diego" };
char* city[N] = {"new-york","chicago", "topeka" };
double demand[N] = { 325.0, 300.0, 275.0 };
double ship[M][N];
int main(int argc, char** argv)
{
FILE *f;
int i,j;
//
// write include file
//
f = fopen("demand.inc","wt");
if (f == NULL) {
perror("fopen");
exit(1);
}
for (i=0; i<N; ++i)
fprintf(f,"%s %g\n",city[i],demand[i]);
fclose(f);
//
// call GAMS
//
system("gams trnsport lo=2");
//
// read solution
//
f = fopen("results.txt","rt");
if (f == NULL) {
perror("fopen");
exit(1);
}
for (i=0; i<M; ++i)
for (j=0; j<N; ++j)
fscanf(f,"%lg",&(ship[i][j]));
fclose(f);
//
// display results
//
for (i=0; i<M; ++i)
for (j=0; j<N; ++j)
printf("%s->%s %g\n",dist[i],city[j],ship[i][j]);
return 0;
}
GamsWorkspace demand
GamsSet j
GamsWorkspace i
GamsInteractive ship
GamsInteractive dist
GamsWorkspace f

Spawning GAMS from Visual Basic

This example shows how GAMS can be invoked from a VB program.

When executed a simple window appears where a GAMS model can be specified and possible other command line options. The [GAMS] button will execute the model.

When the Normal display is used, a console window will be opened. This console window will be closed automatically at the end of the run if the Close process window on completion check box is checked. Minimized does not show a window, but in the taskbar an icon will appear. If the job takes a long time, the user can click on this icon to make the console window visible. The Hidden option prevents any GAMS associated window or icon to appear.

Note: it is required for this program to work, that the GAMS system directory is in the path. If this is not the case, an error code of -1 is returned.

The main program is attached below.

Attribute VB_Name = "vbGAMS"
Option Explicit
' ------------------------------
' Module Name: VBGams
' File: gams.bas
' Version: 1.1
'
'
' Functions:
' VB_Gams32 - sets up process directory, parameter file, and console, and manages
' looping of gams compiler and solver.
'
' --------
' Function VB_Gams32
' Sets up process directory, parameter file, and console, and manages
' looping of gams compiler and solver.
'
' Parameters
' sParams String Command line arguments
' nDisplayMode Integer GAMS_NORMAL, GAMS_MIN, or GAMS_HIDDEN
' bCloseWin Boolean Close the console on completion of gams job
'
' Returns
' 1000 missing input string
' 2000 16 bit spawn failed
' -or-
' nCmexRC + 100 * nVB_GamsRC
'
' nCmexRC : return code from last run of GAMS compile/execute module
' (for DOS/Win XX)
' 0 : normal return
' 1 : solve is next (should not happen)
' 2 : compilation error
' 3 : execution error
' 4 : system limits
' 5 : file error
' 6 : parameter error
' 7 : licensing error
' 8 : GAMS system error
' 9 : GAMS could not be started
'
' nVB_GamsRC: vbGams specific error codes
' 0 : normal return
' 1 : could not create process dir
' 2 : could not run gamsparm script
' 3 : could not append user input to parameter scratch file
' 4 : could not spawn gamscmex.exe
' 5 : could not shell off "gamsnext" script
' 6 : could not delete process directory
'
Public Function VB_Gams32(sParams As String, nDisplayMode As Integer, _
bCloseWin As Boolean) As Long
Dim lAlloc As Long, lRetAPI As Long
Dim nSpawn As Long
If nDisplayMode = GAMS_NORMAL Then ' allocate a console
lAlloc = AllocConsole()
Dim sConTitle As String, hWnd As Long, nTop As Long, nLeft As Long
sConTitle = "VB GAMS "
lAlloc = SetConsoleTitle(sConTitle) ' so I can find it
' if the console window is to stay open, then, need to reposition console window.
If Not bCloseWin Then
Call Sleep(100) ' necessary so console can come up
hWnd = FindWindow(vbNullString, sConTitle)
If hWnd <> 0 Then
nTop = 20
nLeft = 20
lAlloc = SetWindowPos(hWnd, HWND_BOTTOM, nTop, nLeft, 0, 0, SWP_NOZORDER + SWP_NOSIZE)
Call CloseHandle(hWnd)
End If
End If
End If
nSpawn = Spawn("gams.exe " & sParams, nDisplayMode)
If nDisplayMode = GAMS_NORMAL Then
If Not bCloseWin Then
Beep
MsgBox "Click here to close console.", vbOKOnly + vbInformation, "VB GAMS"
End If
lAlloc = FreeConsole()
End If
VB_Gams32 = nSpawn
ShowWait False
End Function

Spawning GAMS from Delphi

This is a Delphi 4 application that has similar features as the Visual Basic application described in the previous paragraph.

Note: it is required for this program to work, that the GAMS system directory is in the path. If this is not the case, an error code of -1 is returned. The main code is attached below.

unit main;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls;
type
TMainForm = class(TForm)
FileEdit: TEdit;
FileOpenDialog: TOpenDialog;
filelabel: TLabel;
BrowseButton: TButton;
CmdLineOptionsEdit: TEdit;
CmdLineLabel: TLabel;
RunButton: TButton;
ReturnLabel: TLabel;
ConsoleComboBox: TComboBox;
ConsoleLabel: TLabel;
CloseConsoleButton: TButton;
procedure BrowseButtonClick(Sender: TObject);
procedure RunButtonClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure CloseConsoleButtonClick(Sender: TObject);
private
{ Private declarations }
function GSExec(const ProgName,ProgParams: string;
const wShowWindow : word;
var rc: integer): integer;
function ExecuteGAMS(
const ProgName,ProgParams: string;
wShowWindow : word; { SW_NORMAL, SW_HIDE or SW_SHOWMINIMIZED }
AutoClose : boolean; { close console at end }
var rc: integer): integer;
public
{ Public declarations }
end;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
function TMainForm.GSExec(const ProgName,ProgParams: string;
const wShowWindow : word;
var rc: integer): integer;
{execute program:
Result: error code for starting the program
rc : error code returned by the program}
var
Command : String;
ProcessInformation : TProcessInformation;
StartupInfo : TStartupInfo;
exitcode : dword;
begin
// Initialise the startup information to be the same as that of the
// calling application. This is easier than initialising the many
// individual startup information fields and should be fine in most
// cases.
GetStartupInfo(StartupInfo);
// StartupInfo.wShowWindow determines whether the called application
// will be initially displayed normal, maximises, minimised or some
// other subtle variations
StartupInfo.wShowWindow := wShowWindow;
StartupInfo.dwFlags := StartUpInfo.dwFlags or STARTF_USESHOWWINDOW;
Command := ProgName + ' ' + ProgParams;
if not CreateProcess(
Nil, {ApplicationName}
PChar(Command), {lpCommandLine }
Nil, {lpProcessAttributes}
Nil, {lpThreadAttribute}
false, {bInheritedHandles}
NORMAL_PRIORITY_CLASS, {dwCreationFlags}
Nil, {lpEnvironment}
Nil, {lpCurrentDirectory}
StartupInfo, {lpStartupInfo}
ProcessInformation {lpProcessInformation}
)
then
begin
rc := 0;
Result := GetLastError {failed to execute}
end
else
begin
with ProcessInformation
do begin
WaitForSingleObject(hProcess,INFINITE);
GetExitCodeProcess(hProcess,exitcode);
CloseHandle(hThread);
CloseHandle(hProcess);
end;
Rc := exitcode;
Result := 0;
end;
end;
function TMainform.ExecuteGAMS(
const ProgName,ProgParams: string;
wShowWindow : word; { SW_NORMAL, SW_HIDE or SW_SHOWMINIMIZED }
AutoClose : boolean; { close console at end }
var rc: integer): integer;
var ok : BOOL;
begin
if AutoClose then begin
{ this is the easy one }
result := GSExec(ProgName, ProgParams, wShowWindow, rc);
exit;
end;
{ in the case we want to let the user close the window,
we need to allocate the console ourselves }
ok := AllocConsole();
result := GSExec(ProgName, ProgParams, wShowWindow, rc);
{ if our console was used, show button to get rid of it }
if (ok) then
CloseConsoleButton.Enabled := true;
end;
procedure CdTemp;
{ cd to windows temp directory }
const maxpath=260;
var path : string;
begin
setlength(path,maxpath);
GetTempPath(maxpath,Pchar(path));
path := ExpandFileName(path); { just to be sure }
ChDir(path); { will also change drive }
end;
procedure TMainForm.BrowseButtonClick(Sender: TObject);
begin
{ popup file open dialog }
if FileOpenDialog.Execute then
FileEdit.text := FileOpenDialog.Filename;
end;
procedure TMainForm.RunButtonClick(Sender: TObject);
var
rc : integer; { return code from GAMS.EXE }
result : integer; { return code from GsExe }
params : string; { command line params for GAMS.EXE }
s : string; { for assembly of error messages }
wShowWindow: word;
AutoClose : boolean;
begin
{ set current directory }
CdTemp;
{ extract command line parameters from edit controls }
{ there may be blanks in the filenames, so add quotes }
params := '"' + FileEdit.Text + '" ' + CmdLineOptionsEdit.Text;
{ get wShowWindow information }
case ConsoleComboBox.ItemIndex of
0, 1 : wShowWindow := SW_NORMAL;
2 : wShowWindow := SW_SHOWMINIMIZED;
3 : wShowWindow := SW_HIDE;
end;
AutoClose := not (ConsoleComboBox.ItemIndex = 1);
{ inform user GAMS is running }
ReturnLabel.Caption := 'Running GAMS...';
ReturnLabel.Font.Color := ClGreen;
Refresh;
{ run GAMS.EXE }
result := ExecuteGAMS('gams.exe', params, wShowWindow, autoClose, rc);
{ check for results }
if (result <> 0) then begin
str(result,s);
ReturnLabel.Caption := 'Exec failed: result = '+s;
ReturnLabel.Font.Color := ClRed;
end else if (rc <> 0) then begin
str(rc,s);
ReturnLabel.Caption := 'GAMS failed: rc = '+s;
ReturnLabel.Font.Color := ClRed;
end else begin
ReturnLabel.Caption := 'OK';
ReturnLabel.Font.Color := ClBlack;
end;
end;
procedure TMainForm.FormCreate(Sender: TObject);
begin
ConsoleComboBox.ItemIndex := 0;
end;
procedure TMainForm.CloseConsoleButtonClick(Sender: TObject);
begin
FreeConsole();
CloseConsoleButton.Enabled := false;
end;
end.

Spawning GAMS from Visual C++

In "this example" we call GAMS from a simple Visual C++ application.

The additional concern is here that we want to intercept the GAMS screen output so it can be written to a multi-line edit control. For more information about the used technique see the Microsoft publication: [HOWTO: Spawn Console Processes with Redirected Standard Handles (Q190351)] (https://support.microsoft.com/en-us/kb/190351) in the Microsoft Knowledge Base.

Spawning GAMS from C#

Below is a simple example:

using System.Diagnostics;
void RunGamsModel()
{
Process p = new Process();
p.StartInfo.FileName = gamsexe;
p.StartInfo.WorkingDirectory = tempdir;
p.StartInfo.Arguments = "\"" + modelname + "\" LO=0 --runid="+runid;
p.Start();
p.WaitForExit();
}
GamsInteractive p

The command line formed here is gams modelname LO=0 –runid=xxx. The option LO (logoption) will disable writing a log, and the option –runid passes a parameter to the GAMS model (inside the model you can access this through %runid%).

Spawning GAMS from Java

Java has a class Runtime that implements spawning of processes using the Exec() method. This is quite trivial to use in applications, but in applets the default security settings don't allow this operation (in general). The problem can be solved by loading the applet from a local drive or by using a signed applet. Similarly if the Java classes are loaded from inside a database, this operation may require additional privileges. For an example see the previous section, where GAMS is called from Oracle using a Java Stored Procedure. The relevant code may look like:

import java.io.File;
class RunGAMS {
public static void main(String[] args) {
System.out.println("Start");
String[] cmdArray = new String[5];
cmdArray[0] = "<PATH/TO/GAMS>" + File.separator + "gams";
cmdArray[1] = "<PATH>" + File.separator + "trnsport.gms";
cmdArray[2] = "WDIR=<PATH>" + File.separator + "TMP";
cmdArray[3] = "SCRDIR=<PATH>" + File.separator + "TMP";
cmdArray[4] = "LO=2";
try {
Process p = Runtime.getRuntime().exec(cmdArray);
p.waitFor();
}
catch (java.io.IOException e )
{
System.err.println(">>>>" + e.getMessage() );
e.printStackTrace();
}
catch (InterruptedException e )
{
System.err.println(">>>>" + e.getMessage() );
e.printStackTrace();
}
System.out.println("Done");
}
}

Below another example, which avoids problems, if the model has a long screen log (the buffer gets filled and locks the execution), which could happen if GAMS does not write the log to the file like above. This example is based on suggestion made by Edson Valle.

import java.io.File;
import java.io.BufferedReader;
import java.io.InputStreamReader;
class RunGAMS {
public static void main(String[] args) {
System.out.println("Start");
String[] cmdArray = new String[5];
cmdArray[0] = "<PATH/TO/GAMS>" + File.separator + "gams.exe";
cmdArray[1] = "<PATH>" + File.separator + "trnsport.gms";
cmdArray[2] = "<PATH>" + File.separator + "tmp";
cmdArray[3] = "LO=3";
try {
Process p = Runtime.getRuntime().exec(cmdArray);
BufferedReader stdInput = new BufferedReader(new InputStreamReader(p.getInputStream()));
String s = null;
while((s=stdInput.readLine()) !=null){
System.out.println(s);
}
p.waitFor();
}
catch (java.io.IOException e )
{
System.err.println(">>>>" + e.getMessage() );
e.printStackTrace();
}
catch (InterruptedException e )
{
System.err.println(">>>>" + e.getMessage() );
e.printStackTrace();
}
System.out.println("Done");
}
}
GamsWorkspace s

For an example of GAMS usage from a Java based server environment see Alexander Sokolov, Information environment and architecture of decision support system for nutrient reduction in the Baltic Sea, Department of Systems Ecology, Stockholm University.

Spawning GAMS from a Web Server

Running GAMS remotely using a Web based thin-client architecture requires that GAMS is executed directly or indirectly from the Web server or HTTP server. A simple way of doing this is via a CGI process. Common Gateway Interface (CGI) programs can be written in many languages such as C, Perl or Delphi. CGI is relatively slow, as for each interaction, even the most simple one, a process needs to be started. Alternatives exist in the form of CGI extensions such as FastCGI or using DLLs or shared libraries. A basic algorithm for a CGI script could be:

  • Create a unique directory, and CD to that directory.
  • Get information from user forms and save it as GAMS readable files.
  • Run GAMS making sure it does not write the log to the screen (i.e. use the option LO=2 (log to a file) or LO=3 (log to stdout)).
  • Let the model write solution info to text files using the PUT statement.
  • Read solution, and create formatted HTML to send back to the user.
  • Remove temp files and directory.

Complications arise when there is a need to show graphics (files need to be stored somewhere and discarded after a while), when jobs take a long time to finish (you will need to add a facility where the user can pick up results at a later moment) or when the server resources can be exhausted (e.g. because of a large number of simultaneous users or because of large models).

An important complicating issue in the above list are jobs that take more time to finish: web servers like to respond to the user within a short time, and time out errors will occur if a job takes a long time. The solution is to use a queue based approach. An actual implementation available is the NEOS Server - there is also a GAMS interface to NEOS: KESTREL - Remote Solver Execution on NEOS Servers.

Spawning GAMS from PHP

A minimal example

index.html

<html>
<body>
<form action="calling_gams.php" method="post">
Solve transport with f = <select name="f">
<option>70</option>
<option>80</option>
<option>90</option>
<option>100</option>
</select>
<input type="submit" value="call GAMS"/>
</form>
</body>
</html>

calling_gams.php

<?php
$f = $_POST['f'];
//some model data
$modelfile = 'trnsport_php.gms';
$city = array('new-york','chicago', 'topeka');
$demand = array(325.0, 300.0, 275.0);
//write demand.inc
$fh = fopen('./demand.inc', 'w+');
for($i=0; $i<count($city); $i++){
fwrite($fh, $city[$i]." ".$demand[$i]."\n");
}
fclose($fh);
//write f.inc
$fh = fopen('./f.inc', 'w+');
fwrite($fh, $f);
fclose($fh);
//call gams
system('</path/to/gams>/gams '.$modelfile.' lo=2');
//read solutions
$fh = fopen('./results.txt', 'r');
echo '<p><b>result of '.$modelfile.' (f='.$f.') :</b></p>';
while (!feof($fh)){
$line = fgets($fh);
echo '<p>'.$line.'</p>';
}
fclose($fh);
?>
bool echo

trnsport_php.gms

Sets
       i   canning plants   / seattle, san-diego /
       j   markets          / new-york, chicago, topeka / ;

  Parameters

       a(i)  capacity of plant i in cases
         /    seattle     350
              san-diego   600  /

       b(j)  demand at market j in cases
         /
$include demand.inc
         / ;


  Table d(i,j)  distance in thousands of miles
                    new-york       chicago      topeka
      seattle          2.5           1.7          1.8
      san-diego        2.5           1.8          1.4  ;

  Scalar f  freight in dollars per case per thousand miles  /
$include f.inc
         / ;

  Parameter c(i,j)  transport cost in thousands of dollars per case ;

            c(i,j) = f * d(i,j) / 1000 ;

  Variables
       x(i,j)  shipment quantities in cases
       z       total transportation costs in thousands of dollars ;

  Positive Variable x ;

  Equations
       cost        define objective function
       supply(i)   observe supply limit at plant i
       demand(j)   satisfy demand at market j ;

  cost ..        z  =e=  sum((i,j), c(i,j)*x(i,j)) ;

  supply(i) ..   sum(j, x(i,j))  =l=  a(i) ;

  demand(j) ..   sum(i, x(i,j))  =g=  b(j) ;

  Model transport /all/ ;

  Solve transport using lp minimizing z ;

  Display x.l, x.m ;

  file fout /results.txt/;
  put fout;
  loop((i,j),
    put i.tl:0 '->' j.tl:0 ':  ' x.l(i,j)/;
  );
  put 'cost: 'z.l/;
  putclose;