Aspen SCM‎ > ‎SCM Planning‎ > ‎

7. Representing Multiple Terms for a Decision Variable in a Constraint

Multiple Fragments for a Generic Row or Column

It often happens that a decision variable appears more than once in a constraint. For instance, multi-time-period models typically have decision variables for the quantity of material carried over from one time period to the next. Suppose that the quantity of product p carried over at the end of time period t is zpt. Suppose also that the quantity of product p produced in time period t is ypt. Then a typical material balance constraint is:

zp,t-1 + ypt – zpt = 0        for all p,t

The decision variable z appears twice in the constraint: once as zpt and once as zp,t-1.

Clearly we must have more than a single set TIME to represent the time periods. We could create a second set TIM2 with the same members and declare it to be a subset of TIME. If the two sets have identical elements, it makes no difference whether we declare TIM2 as a subset of TIME or TIME as a subset of TIM2. But if TIM2 excludes some elements of TIME, e.g. the first, it must clearly be declared as the subset.

As the subscripts of the ypt decision variables and the constraints are straightforward, in implementing them we shall use TIME as the index set. 

There are now (at least) three ways that we can proceed:
  • define the generic column for z as indexed by TIM2 and rewrite the constraint so that the multiple terms are coalesced into one;
  • define two generic columns for z, one indexed by TIME and one by TIM2, each of which has an entry in a single generic row;
  • use a single generic column for z indexed by TIME and define two generic rows, one indexed by TIME and one by TIM2.
Apart from the first, these approaches involve having more than one generic row or column, (i.e. element of ROW or COL) which generates the same row or column in the matrix. We shall refer to these as generic row or column fragments.

Coalescing into Single Terms with Complex Coefficients

We can write our constraint as:

ypt +  Σt' ZCOEFtt' zpt' = 0        for all p,t

where ZCOEFtt' = +1 if t' = t – 1;
                               –1 if t' = t;
                                0  otherwise

We declare the generic column for ypt to be indexed by the sets PROD and TIME; the generic column for zpt' to be indexed by PROD and TIM2; and the generic row to be indexed by PROD and *TIME. We then need to set up the table ZCOEF(TIME,TIM2) to have the required values.

Note that it causes no difficulty that the time index for the generic column for ypt is TIME while that for the  generic row is *TIME. When GEN loops over TIME for ypt it sets its internal index. When it comes to work out which rows to generate, it ignores the no-match marker on *TIME for the generic row as the internal index of TIME is already set.

In practice our implementation isn’t a complete solution: we need to handle the special case when t = 1. In this case we have a constant opening stock, say OSTOCKp instead of the decision variable zp0 (i.e. zp,t-1 for t = 1). To handle this we redefine OSTOCKpt (note the extra subscript “t”) as the opening stock of p when t = 1; and 0 otherwise. Then our constraint is

ypt +  Σt' ZCOEFtt' zpt' = – OSTOCKpt         for all p,t

This can be implemented directly without difficulty.

The advantage of this approach is that each generic row and column is defined only once and each generic column has only one entry in a generic row. The disadvantage is that if we look at COEF we can’t see immediately what is going on.

Note that because all tables in Aspen SCM are essentially 2-dimensional, we did not increase complexity by adding t as a dimension to OSTOCKp; without it we would have had to define the second dimension as 1 and hold the data as OSTOCKp1.

If the opening stock were already 2-dimensional, we should not want to add a further dimension. In that case we would specify ROWS(*,TABL) for this generic row as a table, e.g. BALPOL. We would define BALPOL indexed by (TIME,1) and configure it so that for t > 1, BALPOL takes the value EQ_0, i.e. the standard policy that a row equals 0. For t = 1 we configure BALPOL to take the value, say, OST; we define this in POLI to mean an equality row with the right-hand side set to –OSTOCK.

Splitting Generic Columns into Multiple Fragments

This might appear to be the natural approach. We define two generic column fragments for z, one indexed by TIME and one by TIMP, the set of preceding time periods. TIMP is a proper subset of TIME as it excludes the final time period, t FINAL. Our constraint is:

zpt' + ypt – zpt = 0        for all p,t where t' = t – 1

We define the generic column for ypt to be indexed by the sets PROD and TIME; the generic column fragment for  zpt to be indexed by PROD and TIME; and the generic column fragment for zpt' to be indexed by PROD and TIMP. As we want zpt' to have entries in rows for t = t' + 1, we define the generic row to be indexed by PROD and *TIME.

As noted in the discussion on coalescing multiple terms, no difficulty arises from having TIME as the time index in the generic columns for ypt and zpt while *TIME is the time index of the generic row. We set the entries in COEF as +1 and –1 respectively.

Now consider the zpt' decision variables. These are represented by our generic column fragment which is indexed by PROD and TIMP. We need to get coefficients +1 for it in the constraints for time period t where t = t’ + 1. We rewrite our constraint as

Σt' ZCOEFt't zpt' + ypt – zpt = 0        for all p,t

where ZCOEFt't =  +1 if t = t' + 1;
                                  0  otherwise

We could implement this by defining ZCOEF as a 2-dimensional table indexed by TIMP and TIME and using it as the entry in the COEF table.

A more efficient way to implement this is to use the policy chain for the zpt' generic column fragment to establish the value of the t subscript for the generic row in which it appears. Then the coefficient specified in COEF can be +1 and we no longer need to specify the no-match flag when defining the generic row.

To do this we set COLS(*,TABL) for the generic column fragment for zpt' = TIMPT where TIMPT is a 2-dimensional incidence table. It has non-blank values only for t = t' + 1. As there is only a single non-blank value for each t', we can take the set TIMP as the rowset of TIMPT rather than having an independent rowset. Then the colset of TIMPT consists of just $ENTRY and TIME rather than TIMP, TIME and $ENTRY.

Table TIMPT(TIMP,TIMPT.H)
  $ENTRY TIME
 1 POS
 2
 2 POS 3
 3 POS 4
 4 POS 5

The values which we specify in $ENTRY are whatever POL we want for the z variables, e.g. POS. If we have defined TIMP to exclude the final element of TIME, we are done; otherwise, we specify its value in $ENTRY as blank so that no zpt' column is generated for it.

As with coalescing multiple terms into single entries, we need to make provision for the special case where t = 1 and –the opening stock has to appear on the right hand side in lieu of a zp0 decision variable. We can do this in either of the ways described there.

The most significant disadvantage to splitting generic columns into multiple fragments is that as GEN works column-wise, it is less efficient than splitting generic rows. Set against this, the implementation is closer to the mathematical formulation and so is easier to debug and maintain. In this example the generic columns are sufficiently simple that efficiency is not an issue. But where there are complex look-ups in generating columns or the matrix is very large, efficiency might lead us to prefer to split the generic row.

An issue which arises with splitting both generic rows and columns is that the policy of specific rows and columns in the matrix may be defined in more than one way. If the policy chain for that instance of zp1 from zpt' resolves as POS and the policy chain for zp1 from zpt resolves as something else, which is to be used? The answer is that the values from the policy for the last zp1 which is generated are used. To avoid problems, the policy chains for each generic column fragment should be configured so that for each generated column, the maximum, minimum and cost have the same value regardless of the fragment from which the values were calculated.

Splitting Generic Rows into Multiple Fragments

The column-wise approach of Aspen SCM Planning makes it more efficient to represent multiple terms in a constraint by splitting generic rows into fragments than by splitting generic columns. This means that each specific column is only generated once.

For our constraint

zpt' + ypt – zpt = 0        for all p,t where t' = t – 1
 
we shall define generic columns for y and z each indexed by PROD and TIME. We now define two generic row fragments, one indexed by PROD and TIME, the other by PROD and TIMN, the set of next time periods (it would need to be *TIMN if we didn’t use an incidence table to induce the value of TIMN from that of TIME).

As with splitting the generic column, we induce the value of TIMN from TIME by setting COLS(*,TABL) for the generic column to a 2-dimensional incidence table TIMEN. We shall take TIME itself as the rowset of TIMEN as there is only one  $ENTRY in the table for each element of TIME.

Table TIMEN(TIME,TIMEN.H)
  $ENTRY TIMN
 1 POS
 2
 2 POS 3
 3 POS 4
 4 POS 5
 5 POS
 ???

Suppose that, as shown, we are using the standard policy POS for the z decision variables. Then for each value of t we set TIMEN(t,$ENTRY) = POS. For t < tFINAL (i.e. time periods before the final one, taken as 5 in our example) we set TIMEN(t,TIMN) = t + 1. What value should we give to TIMEN(tFINAL,TIMN)? Ideally we simply wouldn’t define it, i.e. we would only define the table TIMEN for t < tFINAL. But if we did this, we would lose the zpt decision variable for tFINAL, so this is no good. Nor can we set TIMEN(tFINAL,TIMN) to be some junk value, as the integrity-checking on multi-dimensional sets requires it to be an element of the set TIMN.

So we practise some sleight of hand. We set TIMEN(tFINAL,TIMN)  = 1 (or whatever the first time period is called). Then we arrange for the policy chain for the generic row fragment indexed by TIMN = 1 to resolve null, so that that specific row fragment doesn’t exist. We shan’t want it anyway, as the row fragments indexed by TIMN are for the next time period and the first one that we shall want is that for the next time period after the first, i.e. t = 2.

We can now define the entries in COEF for the generic column for zpt to be –1 for the generic row indexed by PROD and TIME and +1 for the generic row indexed by PROD and TIMN.

As with splitting generic columns it is possible that the policy of specific rows in the matrix may be defined in more than one way. This is made more likely because we may be defining different policies for the first time period from later ones and practising sleight of hand with the policy chains for the separate row fragments.

Where a specific row is defined by more than one row fragment, it takes the row sense (E, L or G) from the first fragment which generates the row. It takes the right-hand side from the last fragment which generates the row, except that where the right-hand side evaluates as zero, this is ignored. Where the policy chain for a specific row resolves null (as for TIMN = 1 above), the specific row is never generated for that column and so there is no need to determine which sense and right-hand side to use. Note that as rows are generated inside the loops for columns, it does not follow that the first generic row fragment to appear in the set ROW will give rise to the sense for a specific row.

This approach is the most intricate, and also the most efficient. It is the hardest to understand, especially when trying to maintain legacy models. It is used in the Teapot Refinery Example in the Aspen SCM Planning Help and in most multi-time-period models built by AspenTech.


Back                                Next
Comments