DIYで使う木材を無駄なく買いたい話—ビンパッキング問題で材料カットの組合せを最適化—

さて、「段取り八分、仕事二分」という言葉がありますが、とくに時間的に限られたリソースをうまく回さなければならない週末DIYerとしては、つねに「段取り十分、仕事外注」くらいの勢いでやっていきたいところです。 (外注したらDIYじゃないとか、細かいことは気にせず)

f:id:at_you_key:20190527235930j:plain
図はイメージです

そんなわけで今日は、DIYで木材(SPF材とか)を使うときに、材料をムダなく買ってムダなく使うための上級呪文を紹介したいと思います。

「大きい棚を作りたいけど、部材が多くて材料のカットの割り付けがめんどくさい! 材料買うのは最小限にしたいし、概算金額もバババッと出したい!」 そんな時、この記事がその助けになるでしょう。

定尺の木材、SPF材について

本題に入る前に、主要登場品目の紹介をしておきましょう。

ここで言うSPFってのは、日焼け止めの強さのことじゃなくて、「ツーバイフォー工法」に使われる建材用の木材のことです。

どこのホームセンターでもほとんど必ず置いてあって、流通が多くて安いので、この規格サイズの組み合わせで設計すれば、材料費をけっこう安く抑えることができます。

nodoame.net

ちなみに僕は、ワンバイフォーを使うことが多いです。

"仕事外注"(ホームセンター最高)

さてさて、ワンバイフォーを使うていで本棚の設計ができたとします。

f:id:at_you_key:20190528000309j:plain
こういうのはアナログにやる派

続く工程で面倒なのがカットです。 とくに本棚を作るときは部材の直角・直線が命ですが、手鋸で直線をビシッと決めるのはなかなか大変だし、なによりあんまり楽しくないです。

そこで活用したいのがホームセンターの加工サービス。 木材を買って簡単な図を描いて渡すだけで、1カット数十円でサクッと精度よく切ってくれるので、うまく使えば自前の工具がドライバー1本でも、大抵のものは作れてしまうでしょう。

"段取り十分"(材料カットの最適化)

こっちが本題です。 加工はホームセンターに任せるとして、材料が何本いるかとか、カットの割り振りは自分で決めないといけません。

そして、部材が多いほど難しくめんどくさくなっていきます。

その悩みを解決してくれそうなのが「ビンパッキング問題」です。

ビンパッキング問題

ja.m.wikipedia.org

概要としては「重さがバラバラなビンを、積載量が決まってるトラックに積んで運ぶとき、トラックの台数が最小で済む組み合わせを求める」っていう問題なんですけど

f:id:at_you_key:20190528212134j:plain
イメージです

次のように読み換えると、やりたいことそのまんまって感じですよね!

  • トラック→定尺材
  • 重さ→長さ
  • ビン→部材

ナップサック問題とか、巡回セールスマン問題とかの仲間で、"離散数学の組合せ論の中のNP困難問題" というジャンルだそうです、世の中にはいろんな学問があるんだなぁ。

名前がかっこいいので少し調べてみましたが、正直全然わかりませんでした。まあ、今回は数学世界の探求が目的ってわけじゃないので、使えればよしとします。

ビンパッキング問題を解く強力な呪文「Ortoolpy」

というわけで、難しいところの理解はすっ飛ばして、ラクして簡単にビンパッキング問題を解く方法が知りたい。

そんな堕落したモチベーションをもってGoogle先生に教えを請うたところ、そんな僕にぴったりの、組み合わせ最適化をカンタンに解くためのPythonライブラリを授けてくれました!(厳密にはライブラリではないのかな?)

pypi.org

前置きがだいぶ長くなりましたが、こちらが今日の主役「Ortoolpy」です。

ここからはもう、サクッとツールを使って、バババッと答えを出していきましょう。

(ここから先は、PythonでHello Worldできる程度の知識があれば読み解けると思います。)

www.google.co.jp

ortoolpyのインストール

Ortoolpyのインストールはこのコマンドで一発です。

pip install ortoolpy

使い方

見ての通り、binpacking(入れ物の大きさ,荷物のリスト)とやるだけです。

from ortoolpy import binpacking

resault = binpacking(1820, [780, 560, 780, 780, 600, 580, 580, 780, 800, 800, 1280])

print(resault)

# 実行結果:[[1280], [780, 780], [780, 780], [800, 800], [560, 600, 580], [580]] 

超簡単ですね、やりたい事があっけなくできてしまいました。

おしまい


ではあまりにあっけないので、ついでに結果をソートしたりとか、金額の見積もりなんかを追加してみました。

あと、リストの並び順によって結果の精度が少し変わるようなので、リストをシャッフルしながら数回繰り返して、一番材料が少なくすむ組み合わせを採用するようにしてみます。

そのコードがこちら。

from ortoolpy import binpacking
import random

# 材料の定尺、6フィート
material_length = 1820

# 材料の単価、ホームセンターで調べる
unit_price = 600

# カット単価、これもホームセンターで調べる
cut_price = 50

# 鋸の刃幅、ホームセンターの電ノコはだいたい3mmみておけばOK
blade_width = 3

# 切り出したい部材の長さリスト、3セットにしてみた
parts_list = [78, 56, 78, 78, 60, 78, 560, 780,
780, 600, 580, 580, 780, 800, 800, 1280] * 3

# 計算のために部材それぞれに刃幅を足す
parts_list = list(map(lambda l: l + blade_width, parts_list))

# 試行回数
try_count = 5

# 試行結果を入れていくためのリスト 
resault = []

# 試行回数ぶん回して一番良い結果を採用
for i in range(try_count):

# ループごとに部材のリストをシャッフルする
random.shuffle(parts_list)

# ビンパッキング問題を解いてもらう
packed = binpacking(material_length, parts_list)
print('{0}回目\t{1}本'.format(i + 1, len(packed)))

resault.append(packed)

# 結果を表示する
print('-----------------------------------------------------')

# 材料の本数が少ない順にソート
resault.sort(key=lambda x: len(x))

# カットが少ない(リストが小さい)順にソート
resault[0].sort(key=lambda x: len(x))

# 必要な材料の本数
material_quantity = len(resault[0])

# 材料金額
material_price_total = len(resault[0])*unit_price

# カット回数
cut_count = sum([len(v) - 1 for v in resault[0]])

# カット金額
cut_price_total = cut_count * cut_price

print('必要本数:{0}本\t材料小計:{1}円'.format(material_quantity, material_price_total))
print('カット回数:{0}\t加工費小計:{1}円'.format(cut_count, cut_price_total))
print('合計:{0}円'.format(material_price_total + cut_price_total))

print('-----------------------------------------------------')
# カットの内訳を表示
for i, stuff in enumerate(resault[0]):

# 部材を長い順にソート
stuff.sort(reverse=True)
stuff_length = list(map(lambda l: l - blade_width, stuff))
print(
'No.:{0}\t部材計:{1}mm\t内訳:{2}\t端材:{3}mm'
.format(i + 1, sum(stuff_length), stuff_length, material_length - sum(stuff))
)

実行結果

試行回数は5回にしてみました、部材の数が多いので少しだけ結果にブレがありますね。

その中から狙い通り、材料をより少なくできる組み合わせを選べているようです。

1回目 16本
2回目 15本
3回目 15本
4回目 16本
5回目 15本
-----------------------------------------------------
必要本数:15本 材料小計:9000円
カット回数:33 加工費小計:1650円
合計:10650円
-----------------------------------------------------
No.:1 部材計:1280mm 内訳:[1280] 端材:537mm
No.:2 部材計:1280mm 内訳:[1280] 端材:537mm
No.:3 部材計:1280mm 内訳:[1280] 端材:537mm
No.:4 部材計:780mm 内訳:[780] 端材:1037mm
No.:5 部材計:1638mm 内訳:[780, 780, 78] 端材:173mm
No.:6 部材計:1638mm 内訳:[780, 780, 78] 端材:173mm
No.:7 部材計:1780mm 内訳:[600, 600, 580] 端材:31mm
No.:8 部材計:1780mm 内訳:[580, 580, 560, 60] 端材:28mm
No.:9 部材計:1798mm 内訳:[580, 580, 560, 78] 端材:10mm
No.:10 部材計:1796mm 内訳:[600, 580, 560, 56] 端材:12mm
No.:11 部材計:1756mm 内訳:[800, 800, 78, 78] 端材:52mm
No.:12 部材計:1756mm 内訳:[800, 800, 78, 78] 端材:52mm
No.:13 部材計:1776mm 内訳:[780, 780, 78, 78, 60] 端材:29mm
No.:14 部材計:1772mm 内訳:[780, 780, 78, 78, 56] 端材:33mm
No.:15 部材計:1794mm 内訳:[800, 800, 78, 60, 56] 端材:11mm

まとめ

カットのスケジュールと見積もりって実際かなり面倒なので、これはめちゃめちゃ便利です、捗ります。

最近はワンバイフォーでこんな棚を作りました、材料費はビスとかダボとか含めて7千円くらい。

f:id:at_you_key:20190528000543j:plain
室内の妙な出っ張りに合わせた設計

ちなみに、SPFは無垢材なので、塗装して仕上げたほうが耐久性が上がります。

この棚はその後「ビネガーステイン」っていう、ビンテージ風に仕上がる塗装で仕上げたので、その話も今度書こうと思います。