3.8.13. パラメータを変更した逐次求解

パラメータを変更した逐次求解を行いたい場合,C++SIMPLE では可変定数 VariableParameter を利用できます. 通常,式に登場する定数は式を定義したときの値で評価されますが,可変定数はこれを後から変更することが できる点で Parameter と異なります. PySIMPLE には C++SIMPLE の可変定数はありませんが,同じ意味の記述は可能です. Nuorium Optimizer C++SIMPLEマニュアルで紹介されている直線の傾き a を変化させながら円と直線の接点を 求めるモデルを書いてみましょう.:

x = Variable()  # 円の x 座標
y = Variable()  # 円の y 座標
p = Problem(type=max)
p += (x - 1)**2 + (y + 0.5)**2 <= 0.25  # (1, -0.5) を中心とする円
for a in range(-5, 5):
    p += -a*x + y, 'obj'
    p.solve(silent=True)
    assert p.status == NuoptStatus.OPTIMAL
    print(f'{a=:2} {x.val:.3f} {y.val:.3f} {p.objective.val:.3f}')

PySIMPLE では通常,目的関数を一度しか設定できませんが,求解をはさむことで再度設定ができるようになります. このモデルの出力は次のようになります.:

a=-5 x.val=1.490 y.val=-0.402 obj.val=7.050
a=-4 x.val=1.485 y.val=-0.379 obj.val=5.562
a=-3 x.val=1.474 y.val=-0.342 obj.val=4.081
a=-2 x.val=1.447 y.val=-0.276 obj.val=2.618
a=-1 x.val=1.354 y.val=-0.146 obj.val=1.207
a= 0 x.val=1.000 y.val=-0.000 obj.val=-0.000
a= 1 x.val=0.646 y.val=-0.146 obj.val=-0.793
a= 2 x.val=0.553 y.val=-0.276 obj.val=-1.382
a= 3 x.val=0.526 y.val=-0.342 obj.val=-1.919
a= 4 x.val=0.515 y.val=-0.379 obj.val=-2.438

上記は目的関数を変更するモデルでしたが,制約式を変更するモデルも見てみましょう. 以下は二次関数の最小値を定義域を変更しながら求めるモデルです.:

x = Variable()
p = Problem()
p += x**2 - 4*x + 8, 'obj'
for a in range(5):
    p += x >= a, 'cons'
    p.solve(silent=True)
    assert p.status == NuoptStatus.OPTIMAL
    del p['cons']  # del p[-1] でも可
    print(f'{a=} {x.val:.2f} {p.objective.val:.2f}')

PySIMPLE では += 演算子では制約式の上書きを行うことはできないため,求解後に del 文を用いて一度削除しています. 一方で, p.constraints['cons'] = x >= a と記述すれば制約式の上書きを行うことが可能です. この場合は del 文は不要となります. このモデルの出力は次のようになります.:

a=0 x.val=2.00 obj.val=4.00
a=1 x.val=2.00 obj.val=4.00
a=2 x.val=2.00 obj.val=4.00
a=3 x.val=3.00 obj.val=5.00
a=4 x.val=4.00 obj.val=8.00

これまでは目的関数・制約式が単純でしたが,複雑な場合には少し注意が必要です. 例えば problem += 重い式 >= 可変定数 のような制約式の場合,可変定数に無関係である重い式がループの度に 評価されてしまいます. これを避けるには heavyexp = 重い式 と重い式をループの外に出し, problem += heavyexp >= 可変定数 と することで重い式の評価を一度に抑えることが可能となります. また,可変定数を用いた制約式が多いようであれば,問題自体を関数化してしまうのもよいでしょう.