オペレーションズ・リサーチ

OR-Toolsで瓶パッキング問題を解く

オペレーションズ・リサーチ

[PR]当サイトはアフィリエイト広告を利用しています。

はじめに

今回もOR-Toolsの練習です。

OR-Toolsには瓶パッキング問題を解く機能があるので、それを利用してみます。

対象にするのはこのブログで何回か取り上げてきたSPF材の最小切り出し問題です。

エクセルソルバーを使ったり、ファーストフィットアルゴリズムを使ったりして解いてきました。

今回はこれをOR-Toolsで解きます。

ここでOR-Toolsのライセンスの関係で必要事項を記載しておきます。

ortools  v9.0.9048
Copyright (c) 2000 The Apache Software Foundation.  All rights
Released under the Apache Software License

http://www.apache.org/licenses/LICENSE-1.1
This product includes software developed by the Apache Software Foundation (http://www.apache.org/). /* ==================================================================== * The Apache Software License, Version 1.1 * * Copyright (c) 2000 The Apache Software Foundation. All rights * reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * 3. The end-user documentation included with the redistribution, * if any, must include the following acknowledgment: * "This product includes software developed by the * Apache Software Foundation (http://www.apache.org/)." * Alternately, this acknowledgment may appear in the software itself, * if and wherever such third-party acknowledgments normally appear. * * 4. The names "Apache" and "Apache Software Foundation" must * not be used to endorse or promote products derived from this * software without prior written permission. For written * permission, please contact apache@apache.org. * * 5. Products derived from this software may not be called "Apache", * nor may "Apache" appear in their name, without prior written * permission of the Apache Software Foundation. * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * ==================================================================== * * This software consists of voluntary contributions made by many * individuals on behalf of the Apache Software Foundation. For more * information on the Apache Software Foundation, please see * <http://www.apache.org/>. * * Portions of this software are based upon public domain software * originally written at the National Center for Supercomputing Applications, * University of Illinois, Urbana-Champaign. */ 

では進めていきましょう。

OR-Toolsでの瓶パッキング問題のチュートリアルとほとんど同じなので詳細はそちらに譲ります。

今回も前回の巡回セールスマン問題のときと同様に大元のコードはチュートリアルにあるので、変更点を中心に記載していきます。

詰める部材一つ一つの登録

まず瓶に詰める(ここではSPF材一本から切り出す)部材一つ一つをリストにしてプログラムに登録します。

def create_data_model():
    """Create the data for the example."""
    data = {}
    weights = [20,20,40,40,60,60,60,60,60,60,60,60,60,60,60,60,60,60,80,80,94,94]
    weights.sort(reverse=True)
    data['weights'] = weights
    data['items'] = list(range(len(weights)))
    data['bins'] = data['items']
    data['bin_capacity'] = 180
    return data

weightsに部材の長さをすべて登録しています。

次の行の

weights.sort(reverse=True)

は必須です。

data[‘bin_capacity’] = 180

で切り出すSPF材一本の長さを登録します。

出力に一行追加

そして計算する部分がこちら。

def main():
    data = create_data_model()

    # Create the mip solver with the SCIP backend.
    solver = pywraplp.Solver.CreateSolver('SCIP')


    # Variables
    # x[i, j] = 1 if item i is packed in bin j.
    x = {}
    for i in data['items']:
        for j in data['bins']:
            x[(i, j)] = solver.IntVar(0, 1, 'x_%i_%i' % (i, j))

    # y[j] = 1 if bin j is used.
    y = {}
    for j in data['bins']:
        y[j] = solver.IntVar(0, 1, 'y[%i]' % j)

    # Constraints
    # Each item must be in exactly one bin.
    for i in data['items']:
        solver.Add(sum(x[i, j] for j in data['bins']) == 1)

    # The amount packed in each bin cannot exceed its capacity.
    for j in data['bins']:
        solver.Add(
            sum(x[(i, j)] * data['weights'][i] for i in data['items']) <= y[j] *
            data['bin_capacity'])

    # Objective: minimize the number of bins used.
    solver.Minimize(solver.Sum([y[j] for j in data['bins']]))

    status = solver.Solve()

    if status == pywraplp.Solver.OPTIMAL:
        num_bins = 0.
        for j in data['bins']:
            if y[j].solution_value() == 1:
                bin_items = []
                bin_weight = 0
                for i in data['items']:
                    if x[i, j].solution_value() > 0:
                        bin_items.append(i)
                        bin_weight += data['weights'][i]
                if bin_weight > 0:
                    num_bins += 1
                    print('Bin number', j)
                    print('  Items packed:', bin_items)
                    print('  Total weight:', bin_weight)
                    print()
        print()
        print('Number of bins used:', num_bins)
        print('sumLength: ', data['bin_capacity'] * (num_bins - 1) + bin_weight)
        print('Time = ', solver.WallTime(), ' milliseconds')
    else:
        print('The problem does not have an optimal solution.')

ほとんど同じですが、

print(‘sumLength: ‘, data[‘bin_capacity’] * (num_bins – 1) + bin_weight)

だけ追加しました。

これは最終的にどれだけSPF材を消費したかを出力しています。

data[‘bin_capacity’]は今回は180です。

num_binsは使った瓶の合計です。ここでは使ったSPF材の本数の合計です。

-1しているのは、最後の瓶以外はすべて消費したものと仮定しているからです。

最後の瓶だけは中途半端に残っているので、その残った長さを計算したいから最後の瓶だけ-1して除外しています。

bin_weightに最後の瓶の消費量が保存されているので、それを足しています。

実行結果の出力

実際にプログラムを動かしてみた出力がこちら。

Bin number 0

Items packed: [0, 2]

Total weight: 174

Bin number 1

Items packed: [1, 3]

Total weight: 174

Bin number 2

Items packed: [4, 5, 6]

Total weight: 180

Bin number 3

Items packed: [7, 8, 9]

Total weight: 180

Bin number 4

Items packed: [10, 11, 12]

Total weight: 180

Bin number 5

Items packed: [13, 14, 15]

Total weight: 180

Bin number 6

Items packed: [16, 17, 18, 20]

Total weight: 180

Bin number 7

Items packed: [19, 21]

Total weight: 60

Number of bins used: 8.0

sumLength: 1320.0

Time = 34 milliseconds

ちゃんと瓶は8本消費、合計消費量は1320と出ました。

以前このブログで計算した値と一致しています。

ソート必須

ところで

weights.sort(reverse=True)

は必須です。

と言った部分を覚えていますでしょうか?

これをプログラムに記述しないで求めた出力がこちら。

Bin number 0

Items packed: [0, 1, 2, 3, 4]

Total weight: 180

Bin number 1

Items packed: [5, 6, 7]

Total weight: 180

Bin number 2

Items packed: [8, 9, 10]

Total weight: 180

Bin number 3

Items packed: [11, 12, 13]

Total weight: 180

Bin number 4

Items packed: [14, 15, 16]

Total weight: 180

Bin number 5

Items packed: [17, 18]

Total weight: 140

Bin number 6

Items packed: [19, 20]

Total weight: 174

Bin number 7

Items packed: [21]

Total weight: 94

Number of bins used: 8.0

sumLength: 1354.0

Time = 40 milliseconds

合計消費量が1354になっています。

これはこれまでにこのブログで求めた消費量より多くなっています。

OR-Toolsのソルバーの中身はわかりませんが、ファーストフィットアルゴリズムのときにソートしたのと同じような現象が起きるようです。

大きい順で計算したほうが詰めるときの最後のほうで大きな部品が残りにくくなるので瓶を効率よく詰めることができるのでしょう。

OR-Toolsで瓶パッキング問題を扱うときは大きい順でソートしてから計算しましょう。

瓶パッキング問題をちゃちゃっと計算したいときにOR-Toolsは使えそうですね。

問題は現実世界で瓶パッキング問題を見つけられるかどうかってことですね。

(申し訳ありません。ご指摘があり訂正させていただきます。工学や建築の分野では結構出てくる問題です。あくまで日常生活で頻出してコンピュータで個人的に計算するような問題を見つけるのが難しいという意味で書きました。私がブログのネタを考える時に身近な例が何かないかな、と考えて、なかなかいい例題が見つからないということをボヤいてしまった結果上のような文言となりました。)

いくつかの規格品からバラバラに分ける、使うということがあれば便利なツールです。