Model Instances (pyEmbMI)

Libinclude file pyembpy.gms for the GAMS Python API class GAMSModelInstance. An instance of this class provides access to a model instance that can be modified and resolved without regenerating the model over and over.

Usage:

$libinclude pyEmbMI myPyMI 'solve statement' [optlist] updateList

Where:

Argument Description
myPyMI Name of the GAMSModelInstance Python identifier.
solve statement Part of the solve statment that gives model name, solver type and optimization direction / variable, e.g. transport minimize z us lp.
optlist Optional list of -optkey=optval option pairs, e.g. -reslim=100 -all_model_types=cplex that build update a GamsOption object used in the instantiate call. See gams.control.options.GamsOptions Class Reference for details.
updateList List of update tuples (see details below):
Variable update tuples: gmsModelVar.Lower|Upper|Fixed|Primal|Dual.gmsPar.Zero|BaseCase|Accumulate
Parameter update pairs: gmsModelPar.Zero|BaseCase|Accumulate

The update tuples contain instructions how to deal with updating variable/equation attributes (mostly bounds) and parameters: If you want to change a parameter the entries holds the syntax: x.action where x is the GAMS name of the parameter and action is the update type of the parameter. This identifies how a parameter is updated if there is no data corresponding to the set intersection defining the parameter.

  • parametername.Zero will set all parameter values that are not explicitly set to zero.
  • parametername.BaseCase will set all parameter values that are not explicitly set to the value from the first model instantiation.
  • parametername.Accumulate will keep all parameter values that are not explicitly set to their last known value.

To update an attribute of a variable/equation 4 things need to be considered:

  • What variable or equation to work with
  • What attribute to change (e.g. upper and lower corresponding bounds)
  • What parameter contains the new values
  • What to do with zeros which is the Zero, BaseCase, and Accumulate from above. This is expressed in the tuple variableName.attribute.parameterWithNewValues.zeroKeyword. For example x.Upper.xUp.BaseCase would reset upper bounds for x to the values in the parameter xup and set the values that are not explicitly set to their last known value.

Hence a call to this include might look like this:

$libinclude pyEmbMI miTransport 'transport minimizing z using lp' -all_model_types=cplex  x.Upper.xUp.BaseCase b.zero


Embedded Code Model Instance Example

The GAMS Embedded Code facility allows to execute foreign code (e.g. Python) while GAMS runs and to exchange data with GAMS without any disk access (e.g. GDX).

In this example we combine the power of the embedded code facility with the GAMS Python OO-API class GAMSModelInstance. An instance of this class provides access to a model instance that can be modified and resolved without regenerating the model over and over.

Here we generate the model instance once by using the libinclude pyEmbMI. The arguments to this call provide all necessary information to instantiate an instance of a GAMSModelInstance. In particular we provide the relevant part of the solve statement as well as a list of modifiers. These are the parameters in the model that are subject to change. In addition we can provide some options belonging to a GAMS/Python OO-API class GAMSOptions via the -key=value pairs. See more about the use of GAMSModelInstance and GAMSOptions in the GAMS/Python OO-API.

Background:

A traditional GAMS implemenation of a scenario loop looks like this:

loop(ScenariosToRun,
   a(i) = newsupply(ScenariosToRun,i);
   b(j) = newdemand(ScenariosToRun,j);
   solve transport using lp minimizing z;
   resultantx(ScenariosToRun,i,j) = x.l(i,j)
);

With the embedded code/GAMSModelInstance solution this loop looks as follows:

$libInclude pyEmbMI tMI 'transport us lp min z' -all_model_types=cplex a.Zero b.Zero
loop(ScenariosToRun,
   a(i) = newsupply(ScenariosToRun,i);
   b(j) = newdemand(ScenariosToRun,j);
   continueEmbeddedCode:
   gams.db['a'].copy_symbol(tMI.sync_db['a'])
   gams.db['b'].copy_symbol(tMI.sync_db['b'])
   tMI.solve()
   tMI.sync_db['x'].copy_symbol(gams.db['x'])
   pauseEmbeddedCode x
   resultantx(ScenariosToRun,i,j) = x.l(i,j);
);

With a little helper function solveMI (see below) this code becomes even more similar:

$libInclude pyEmbMI tMI 'transport us lp min z' -all_model_types=cplex a.Zero b.Zero
loop(ScenariosToRun,
   a(i) = newsupply(ScenariosToRun,i);
   b(j) = newdemand(ScenariosToRun,j);
   continueEmbeddedCode:
   solveMI(tMI,['a','b'],['x'])
   pauseEmbeddedCode x
   resultantx(ScenariosToRun,i,j) = x.l(i,j);
);

In contrast to GUSS/Scenario Solver here we implement the loop logic in GAMS and execute in the loop body the solve method of the GAMSModelInstance class inside the embedded Python code. Rather than using the gams.get|set method of the embedded code facility we use GAMSDatabase.copy_symbol to move data between GAMS (gams.db) and the GAMSModelInstance.sync_db.

Note
Even though we don't exercise the ability in this example, the combination of GAMSModelInstance and embedded code provides a way of defining the scenario n+1 based on the result (primal and dual) of the nth scenario. This is not possible in GUSS/Scenario Solver.

Example:

* GMSPYTHONLIB gets automatically set to use the internal Python installation in sysdir/GMSPython.
$if not setEnv GMSPYTHONLIB $abort.noError Embedded code Python not ready to be used
$log --- Using Python library %sysEnv.GMSPYTHONLIB%

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

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

   b(j) 'demand at market j in cases'
        / new-york   325
          chicago    300
          topeka     275 /;

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;

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

Positive Variable x;

Equation
   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 /;

Set s 'scenarios to run' / base, run1, run2 /;

Table newsupply(s,i) 'updater for a (capacity)'
         seattle  san-diego
   base      350        600
   run1      300        650
   run2      400        550;

Table newdemand(s,j) 'updater for b (demand)'
         new-york  chicago  topeka
   base       325      300     275
   run1       325      300     275
   run2       350      300     250;

$set solverlog
$if set useSolverLog $set solverlog output=sys.stdout
embeddedCode Python:
def solveMI(mi, symIn=[], symOut=[]):
  for sym in symIn:
    gams.db[sym].copy_symbol(mi.sync_db[sym])
  mi.solve(%solverlog%)
  for sym in symOut:
    try:
      gams.db[sym].clear() # Explicitly clear the symbol to ensure setting "writtenTo" flag for sym
      mi.sync_db[sym].copy_symbol(gams.db[sym])
    except:
      pass
pauseEmbeddedCode
abort$execerror 'Python error. Check the log';

$libInclude pyEmbMI tMI 'transport us lp min z' -all_model_types=cplex a.Zero b.Zero

Parameter repX(s,i,j) 'collector for level of x';

loop(s,
   a(i) = newsupply(s,i);
   b(j) = newdemand(s,j);
   continueEmbeddedCode:
   solveMI(tMI,['a','b'],['x'])
   pauseEmbeddedCode x
   repX(s,i,j) = x.l(i,j);
);

option  repX:0:1:2;
display repX;

Set error(s) 'empty solution';
error(s) = sum((i,j), repX(s,i,j)) = 0;
abort$card(error) 'Missing solution for some scenarios', error;

The complete example is also part of the GAMS Model Library, see model [embmiex1] for reference.