CPLEXサンプル ProdPlan（モデル２各期の組立台数、販売台数、在庫数の計画）を読み解く

CPLEXのサンプルのProdPlan（CPLEX OPL CPLEXサンプル MIP）を読み解きます。

<サンプル導入ディレクトリ>opl\examples\opl\ProdPlan\totalprod.mod
<サンプル導入ディレクトリ>opl\examples\opl\ProdPlan\period.mod
にあります。

* 実行環境
    * CPLEX 22.1.1
    * Windows 11 64bit

* サンプル

●コード

https://github.com/hkwd/230823cplexsample/blob/master/231124ProdPlan/period_hkwd.mod

この記事でのサンプルは、オリジナルのサンプルとほぼ同じ内容ですが、日本語コメントを入れたり順序を入れ替えたり、添え字を入れ替えたりして読みやすくしています

●サンプルデータ

https://github.com/hkwd/230823cplexsample/blob/master/231124ProdPlan/period_hkwd_p1solved.dat

https://github.com/hkwd/230823cplexsample/blob/master/231124ProdPlan/period_hkwd.dat

# 問題

コンピューター組み立て工場の3期分の生産計画を作成します。この工場は、MultimediaBusiness、MultimediaHome、および BasicHome という 3 つのコンピューター・モデルを組み立てることができます。
それぞれのモデル毎、期ごとに最小需要数と最大需要数、そして在庫可能数があります。
各期ごとに組立可能な台数があります。
各モデル毎にCPU やモデムなどの必要なコンポーネントは異なります。各コンポーネントは内製、外注それぞれでコストが異なります。

モデルを2段階に分けて生産計画を作っていきます。この記事はモデル２を対象にします。

[モデル１：組み立てる各タイプのコンピューターの総数と、必要なコンポーネントを内製、外注する数を決定します。](https://qiita.com/kawada2017/items/4efe3fede793c9dddcaa)

モデル２：各期間に組み立てる数、販売する数、および在庫数を決定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/45636047-1539-05cc-2da6-ca25c0bc6ce6.png)



## 利用データ
1.  各期毎の最大組み立て可能数

|期間|値|
|:----|:----|
|W1|35|
|W2|20|
|W3|25|


２.  コンピュータータイプのコンポーネントや需要量

|コンピュータタイプ|コンポ―ネント|価格|最大可能在庫数|最小需要W1|最小需要W2|最小需要W3|最大需要W1|最大需要W2|最大需要W3|
|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|
|MultimediaBusiness|{CPU250 Modem56K SoundCard VideoCard CDRW}|8000|5|5|6|7|15|16|17|
|MultimediaHome|{CPU250 Modem56K SoundCard VideoCard}|4000|5|4|5|6|14|15|16|
|BasicHome|{CPU150 Modem288 VideoCard}|2000|5|3|4|5|13|14|15|

３.全期Build数合計
全期Build数合計です。これは前のモデルtotalprod_hkwd.modの解です。これを各期に振り分けていくのがこのモデルの役割になります。

|コンピュータタイプ|TotalBuild（全期Build数合計）|
|:----|:----|
|MultimediaBusiness|48|
|MultimediaHome|20|
|BasicHome|12|

## 決定変数
0. 各期の組立台数
0. 各期の販売台数
0. 各期の在庫数の決定


## 目的関数
目的関数はない。各期に振り分けられる実行可能解を探す。


## 制約
0.  各期のコンピュータータイプごとの販売台数が最大需要数以下
0.  各期のコンピュータータイプごとの販売台数が最小需要数以上
0.  各期のコンピュータータイプごとの在庫数が、各コンピュータータイプ毎の最大在庫数以下
0.  各期の組立台数が最大組立数以下
0.  各期のコンピュータータイプごとの組立台数が全期の総組立台数と等しい
0.  各期のコンピュータータイプごとの売上台数が全期の総組立台数と等しい。売り切らないと前モデルの目的関数の利益を達成できない
0.  1期目の組立台数が1期目の販売数と1期目の在庫数の合計と等しい
0.  2期目以降の組立台数および前期の在庫数が、その期の販売数とその期の在庫数の合計と等しい


## 問題の種類
整数計画

# 解
先に解を紹介します。

## 目的関数
なし

## 決定変数
１. 各期の組立台数、販売台数、在庫数の決定

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/912e98de-4957-97cd-4548-8a5313f28ee1.png)



# OPLコード解説
OPLコードの解説を行っていきます。

## 利用データ

まずコンピュータータイプとコンポーネントのデータの定義を行っています。
データの中身の定義はperiod_hkwd_p1solved.datファイルの中で行っています。前のモデルtotalprod_hkwd.modの解をそのまま使う場合にはperiod_hkwd.datから読みます。
二つのファイルの違いはTotalBuildです。
period_hkwd_p1solved.dat：TotalBuild = [48 20 12];
period_hkwd.dat：TotalBuild = [0 0 0];
単体で動かすためにはTotalBuild = [48 20 12]が必要です。totalprod_hkwd.modの解を使う場合は、totalprod_hkwd.modの中でTotalBuild = [0 0 0]を [48 20 12]に書き換えています。


```java:コンピュータータイプとコンポーネントの定義(mod)
{string} ComputerTypes = ...;
{string} ComponentTypes = ...;
```


```java:コンピュータータイプとコンポーネントの定義(dat)
ComputerTypes = { MultimediaBusiness, MultimediaHome, BasicHome };
ComponentTypes = { CPU150, CPU250, Modem288, Modem56K, SoundCard, VideoCard, CDRW };
```


各コンピュータタイプには、コンポーネントのセット、販売価格、および在庫可能最大数があります。

```java:コンピュータ(mod)
 *   --------------------------------------------------- */
tuple computersToBuild
{
  {string} components;
  int      price;
  int      maxInventory;
}

computersToBuild Computers[ComputerTypes] = ...;
```

```java:コンピュータ(dat)
Computers = [<{CPU250, Modem56K,SoundCard,VideoCard,CDRW},8000,5>,
                     <{CPU250,Modem56K,SoundCard,VideoCard},4000,5>,
                     <{CPU150,Modem288,VideoCard},2000,5>];

```

問題ブラウザで見ると以下のようになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/f5af3cc6-9d35-37dd-62d2-6d3ce5d5c838.png)

期間を定義しています。3週間と定義しています。

```java:期間(mod)
//期間数
int NbPeriods = ...;
range Periods = 1..NbPeriods;
```
```java:期間(dat)
NbPeriods = 3;
```
問題ブラウザで見ると以下のようになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/eda21fa3-751a-53cf-a289-b1aa011c9046.png)

各コンピューターの種類には、各期間に販売できる最大数量と最小数量があります。 これらの値は、各コンピューターの種類の総組立台数を計算するために使用されます。 各期間の最大組立可能台数もあります。

```java:需要数、組立可能台数(mod)
//期ごとの最大の組立可能台数
int MaxBuildPerPeriod[1..NbPeriods] = ...;

//コンピュータの種類と期ごとの最小最大の需要量
int MinDemand[ComputerTypes][1..NbPeriods] = ...;
int MaxDemand[ComputerTypes][1..NbPeriods] = ...;
```
```java:需要数、組立可能数(dat)
MaxBuildPerPeriod = [35 20 25];

MinDemand = [[5,6,7],
         [4,5,6],
         [3,4,5]];

MaxDemand = [[15,16,17],
         [14,15,16],
           [13,14,15]];
```

問題ブラウザで見ると以下のようになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/125340b1-cbc8-111f-52d3-d398535b5774.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/d15d483a-267f-5809-20d1-fc3ba255ebef.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/fecd445c-810b-a56c-6c44-f6400b0fb8d1.png)

ここまでは前のモデル用のデータファイルのproduction_hkwd.datにもあった項目です。
次に「全期分のコンピュータータイプ毎の組立台数の合計」を定義しています。

「全期分のコンピュータータイプ毎の組立台数の合計」は、単体で動かす場合は[48 20 12]というように値を設定し、前のモデルの解を使う場合はダミー値 [0 0 0]を設定しておきます。

```java:全期分のコンピュータータイプ毎の組立台数の合計(mod)
//前の問題の結果から入力する
float TotalBuild[ComputerTypes] = ...;
```

```java:全期分のコンピュータータイプ毎の組立台数の合計(単体動作用period_hkwd_p1solved.dat)
//単体で動作させるために一つ目の問題の解を入力済み
TotalBuild = [48 20 12];
```

```java:全期分のコンピュータータイプ毎の組立台数の合計(モデル１入力用　period_hkwd.dat)
//一つ目の問題の解を入力するのでダミー値
TotalBuild = [0 0 0];
```


問題ブラウザで見ると以下のようになっています(period_hkwd_p1solved.datの場合)。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/bc1b35dd-2b1d-2f98-a028-4336b29cd6f4.png)


## 決定変数
次に決定変数の定義を行っています。
dvarが決定変数を意味します。

```java:決定変数
//各期間に各タイプのコンピューターを何台構築するか
dvar int+ Build[ComputerTypes][Periods];
//各期間に各タイプのコンピューターを何台販売するか
dvar int+ Sell[ComputerTypes][Periods];
//各期間の終了時に在庫に保持する各コンピューター タイプの数
dvar int+ InStockAtEndOfPeriod[ComputerTypes][Periods];
```

これらの決定変数は、求められた解では以下のようになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/ab387216-98ef-45e5-9e42-e0c5d28b9b76.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/8e352534-ebe0-5b0e-2537-5f167603d00d.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/c460b052-4d03-6a9e-d8a2-bc1883e64bd8.png)



## 目的関数
ありません。

## 制約
次に制約の定義を行っています。
subject toの中カッコで囲みます。
```python:制約（mod）
subject to{

---略----
}

```

ctXXX:で制約にラベルを付けています。


### 制約1:各期のコンピュータータイプごとの販売台数が最大需要数以下

```java:制約1:各期のコンピュータータイプごとの販売台数が最大需要数以下
forall(c in ComputerTypes, p in Periods)
  ctUnderMaxDemand: Sell[c][p] <= MaxDemand[c][p]; 
```
問題ブラウザで見ると以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/770b64c2-afb0-501b-94ee-f39a579d9bc8.png)



図にすると以下のようなイメージになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/f541e63c-29e7-2225-3069-ef643ef110ec.png)


### 制約2:各期のコンピュータータイプごとの販売台数が最小需要数以上

```java:制約2:各期のコンピュータータイプごとの販売台数が最小需要数以上
forall(c in ComputerTypes, p in Periods)
  ctOverMinDemand: Sell[c][p] >= MinDemand[c][p];
```
問題ブラウザで見ると以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/95a94c67-734f-5e38-6ca3-416863c7f7cc.png)

図にすると以下のようなイメージになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/ef183a0d-1edd-c31c-02c0-b9e8e122e69d.png)

### 制約3:各期のコンピュータータイプごとの在庫数が、各コンピュータータイプ毎の最大在庫数以下


```java:制約3:各期のコンピュータータイプごとの在庫数が、各コンピュータータイプ毎の最大在庫数以下
forall(c in ComputerTypes, p in Periods)
  ctComputerTypeInventoryCapacity:     
    InStockAtEndOfPeriod[c][p] <= Computers[c].maxInventory;
```
問題ブラウザで見ると以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/5c9a5e93-0cac-5b58-3838-6bfef071f6c4.png)


図にすると以下のようなイメージになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/f53f9d66-a11d-cbb5-45d6-84309786ca9f.png)


### 制約4:各期の組立台数が最大組立数以下


```java:制約4:各期の組立台数が最大組立数以下
forall(p in Periods)
  ctUnderMaxBuild:sum(c in ComputerTypes) Build[c][p] <= MaxBuildPerPeriod[p];
```
問題ブラウザで見ると以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/368d4027-2a88-4701-273a-bb5f7d9c8e4c.png)



図にすると以下のようなイメージになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/65565270-569a-060b-0184-062f3601e376.png)

### 制約5:各期のコンピュータータイプごとの組立台数が全期の総組立台数と等しい


```java:制約5:各期のコンピュータータイプごとの組立台数が全期の総組立台数と等しい
forall(c in ComputerTypes)
  ctTotalToBuild:      
    sum(p in Periods) Build[c][p] == TotalBuild[c];
```
問題ブラウザで見ると以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/c31837eb-368b-8cb0-02cb-a36c0888c693.png)



図にすると以下のようなイメージになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/dc552636-3bb1-5293-0bbe-614878a1e579.png)

### 制約6:各期のコンピュータータイプごとの売上台数が全期の総組立台数と等しい。売り切らないと前モデルの目的関数の利益を達成できない
前のモデルの目的関数では組立台数から売上を計算していたので、売り切らずに在庫が残ってしまうと、予定の利上げが達成できないことになります。

```java:制約6:各期のコンピュータータイプごとの売上台数が全期の総組立台数と等しい。
forall(c in ComputerTypes)
  ctTotalToSell:      
    sum(p in Periods) Sell[c][p] == TotalBuild[c];    

```
問題ブラウザで見ると以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/37705cc5-b33a-e378-ee78-92f5656d722f.png)


図にすると以下のようなイメージになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/b3a34ce2-7def-97de-0034-b6fe38678b59.png)

### 制約7:1期目の組立台数が1期目の販売数と1期目の在庫数の合計と等しい


```java:制約7:1期目の組立台数が1期目の販売数と1期目の在庫数の合計と等しい
forall(c in ComputerTypes)
  Build[c][1] == Sell[c][1] + InStockAtEndOfPeriod[c][1];
```
問題ブラウザで見ると以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/6743aedc-8b65-6ad4-54ff-91814a54e965.png)

図にすると以下のようなイメージになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/fe3a15be-8da2-aadf-c863-f2930aa74a29.png)

### 制約8:2期目以降の組立台数がその期の販売数とその期の在庫数および前期の在庫数の合計と等しい
「p in 2..NbPeriods」で2期目以降のループ、
「InStockAtEndOfPeriod[c][p-1]」は前期の在庫数を示しています。

```java:制約8:2期目以降の組立台数がその期の販売数とその期の在庫数および前期の在庫数の合計と等しい
forall(c in ComputerTypes, p in 2..NbPeriods)
  ctInventoryBalance:      
    InStockAtEndOfPeriod[c][p-1] + Build[c][p] == 
      Sell[c][p] + InStockAtEndOfPeriod[c][p]; 
```
問題ブラウザで見ると以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/500a3dd2-34f2-c2ce-674a-daba1e3142bb.png)



図にすると以下のようなイメージになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223763/36111e74-71f9-b94d-3563-045decb24a30.png)


## モデルの実行

period_hkwd.modとそのデータのperiod_hkwd_p1solved.datとの組み合わせであれば、このモデル単体で動作するようになっています。
totalprod_hkwd.modから呼び出す場合には、「TotalBuild」 がtotalprod_hkwd.modの解の決定変数から与えられますので、ダミー値の定義されたperiod_hkwd.datで実行するようにしています。




# 完成OPL

製品サンプルとほぼ同じ内容ですが、日本語コメントを入れたり順序を入れ替えたり、添え字を入れ替えたりして読みやすくしています。

```Java:totalprod_hkwd.mod
/*  ----------------------------------------------------
 *   OPL Model for Production planning Example
 *
 *   This model is described in the documentation. 
 *   See IDE and OPL > Language and Interfaces Examples.
 各期の組立台数、販売台数、在庫数の決定
 *   --------------------------------------------------- */

{string} ComputerTypes = ...;
{string} ComponentTypes = ...;

tuple computersToBuild {
   {string} components;
   int      price;
   int      maxInventory;
}
computersToBuild Computers[ComputerTypes] = ...;

//期間数
int NbPeriods = ...;
range Periods = 1..NbPeriods;

//期ごとの最大の組立可能台数
int MaxBuildPerPeriod[Periods] = ...;
//コンピュータの種類と期ごとの最小最大の需要量
int MinDemand[ComputerTypes][Periods] = ...;
int MaxDemand[ComputerTypes][Periods] = ...;

//最大在庫可能数
//int MaxInventory = 15;

//全期分のコンピュータータイプ毎の組立台数の合計。前の問題の結果から入力する
float TotalBuild[ComputerTypes] = ...;



////決定変数
/*  ----------------------------------------------------
 *   Variables:
 *   build --   How many of each computer type to build
 *         in each period
 *   inStockAtEndOfPeriod --   How many of each computer 
 *   type to hold over in inventory at the end of each
 *         period
 *   --------------------------------------------------- */
//各期間に各タイプのコンピューターを何台構築するか
//dvar float+ Build[ComputerTypes][Periods];
dvar int+ Build[ComputerTypes][Periods];
//各期間に各タイプのコンピューターを何台販売するか
//dvar float+ Sell[ComputerTypes][Periods];
dvar int+ Sell[ComputerTypes][Periods];
//各期間の終了時に在庫に保持する各コンピューター タイプの数
//dvar float+ InStockAtEndOfPeriod[ComputerTypes][Periods];
dvar int+ InStockAtEndOfPeriod[ComputerTypes][Periods];

////目的関数はない。各期に振り分けられる実行可能解を探す。

////制約
subject to {
  //制約0:最大在庫可能数より各期の在庫数が小さい
  //制約3でカバーされるためコメントアウト
  //forall(p in Periods)  
  //  ctInventoryCapacity:  
  //    sum(c in ComputerTypes) InStockAtEndOfPeriod[c][p] <= MaxInventory;

  //制約1:各期のコンピュータータイプごとの販売台数が最大需要数以下
  forall(c in ComputerTypes, p in Periods)
    ctUnderMaxDemand: Sell[c][p] <= MaxDemand[c][p];
     

  //制約2:各期のコンピュータータイプごとの販売台数が最小需要数以上
  forall(c in ComputerTypes, p in Periods)
    ctOverMinDemand: Sell[c][p] >= MinDemand[c][p];
      
  //制約3:各期のコンピュータータイプごとの在庫数が、各コンピュータータイプ毎の最大在庫数以下
  forall(c in ComputerTypes, p in Periods)
    ctComputerTypeInventoryCapacity:     
      InStockAtEndOfPeriod[c][p] <= Computers[c].maxInventory;

  //制約4:各期の組立台数が最大組立数以下　**hkwd追加
  forall(p in Periods)
    ctUnderMaxBuild:sum(c in ComputerTypes) Build[c][p] <= MaxBuildPerPeriod[p];
      
  //制約5:各期のコンピュータータイプごとの組立台数が全期の総組立台数と等しい
  forall(c in ComputerTypes)
    ctTotalToBuild:      
      sum(p in Periods) Build[c][p] == TotalBuild[c];

  //制約6:各期のコンピュータータイプごとの売上台数が全期の総組立台数と等しい。売り切らないと前モデルの目的関数の利益を達成できない　**hkwd追加
  forall(c in ComputerTypes)
    ctTotalToSell:      
      sum(p in Periods) Sell[c][p] == TotalBuild[c];    

  //制約7:1期目の組立台数が1期目の販売数と1期目の在庫数の合計と等しい
  forall(c in ComputerTypes)
    ctInventory1wBalance:      
    Build[c][1] == Sell[c][1] + InStockAtEndOfPeriod[c][1];
     
  //制約8:2期目以降の組立台数および前期の在庫数が、その期の販売数とその期の在庫数の合計と等しい
   forall(c in ComputerTypes, p in 2..NbPeriods)
     ctInventoryBalance:      
       InStockAtEndOfPeriod[c][p-1] + Build[c][p] == 
         Sell[c][p] + InStockAtEndOfPeriod[c][p]; 
}
```

データは以下です。

```Java:production_hkwd.dat
ComputerTypes = { MultimediaBusiness, MultimediaHome, BasicHome };
ComponentTypes = { CPU150, CPU250, Modem288, Modem56K, SoundCard, VideoCard, CDRW };

NbPeriods = 3;
//MaxBuildPerPeriod = [50 10 20]; //この制約だと解がなかった
MaxBuildPerPeriod = [35 20 25];

MinDemand = [[5,6,7],
         [4,5,6],
         [3,4,5]];

MaxDemand = [[15,16,17],
         [14,15,16],
           [13,14,15]];

Computers = [<{CPU250, Modem56K,SoundCard,VideoCard,CDRW},8000,5>,
                     <{CPU250,Modem56K,SoundCard,VideoCard},4000,5>,
                     <{CPU150,Modem288,VideoCard},2000,5>];
                     


//一つ目の問題の解を入力済み
TotalBuild = [48 20 12];
//TotalBuild = [0 0 0]; //totalprod_hkwd.modから呼び出す場合は一つ目の問題の解を入力するのでダミー値
```

# 参考
#### 製造：生産計画- IBM Documentation 

https://www.ibm.com/docs/ja/icos/20.1.0?topic=library-manufacturing#usroplsamples.uss_opl_modlib.1016611__usroplsamples.uss_opl_modlib.1016871


#### CPLEXサンプルを読み解く記事一覧

https://qiita.com/kawada2017/items/bdb85f66e5f1de6039d6