行列は大文字で、ベクトルは小文字で示します。
ベクトルv
の線形不等式の次のシステムを解く必要があります。
_min(rv - (u + Av), v - s) = 0
_
ここで、_0
_はゼロのベクトルです。
ここで、r
はスカラー、u
とs
はベクトル、A
は行列です。
_z = v-s
_、_B=rI - A
_、_q=-u + Bs
_を定義すると、前の問題を 線形相補性問題 として書き直すことができ、たとえばopenopt
からLCPソルバーを使用したいと考えています。
_LCP(M, z): min(Bz+q, z) = 0
_
または、行列表記の場合:
_z'(Bz+q) = 0
z >= 0
Bz + q >= 0
_
問題は、私の連立方程式が巨大であるということです。 A
を作成するには、
A11
_を使用して、_A12
_、_A21
_、_A22
_、_scipy.sparse.diags
_の4つの行列を作成します。A = scipy.sparse.bmat([[A11, A12], [A21, A22]])
としてスタックします。A
が対称ではないため、QP
問題への効率的な変換が機能しないことも意味します)_openopt.LCP
_は、スパース行列を処理できないようです。これを実行すると、コンピューターがクラッシュしました。通常、A.todense()
はメモリエラーにつながります。同様に、_compecon-python
_は、スパース行列のLCP
問題を解決できません。
この問題に適した代替のLCP
実装は何ですか?
一般的な「LCPを解決するためのツール」の質問にサンプルデータが必要だとは本当に思いませんでしたが、とにかく、ここに行きます
_from numpy.random import Rand
from scipy import sparse
n = 3000
r = 0.03
A = sparse.diags([-Rand(n)], [0])
s = Rand(n,).reshape((-1, 1))
u = Rand(n,).reshape((-1, 1))
B = sparse.eye(n)*r - A
q = -u + B.dot(s)
q.shape
Out[37]: (3000, 1)
B
Out[38]:
<3000x3000 sparse matrix of type '<class 'numpy.float64'>'
with 3000 stored elements in Compressed Sparse Row format>
_
さらにいくつかのポインタ:
openopt.LCP
_が行列でクラッシュし、続行する前に行列を密に変換すると思いますcompecon-python
_はエラーで完全に失敗します。明らかに密な行列が必要であり、スパース性のフォールバックはありません。B
は正の半確定ではないため、線形相補性問題(LCP)を凸2次問題(QP)と言い換えることはできません。この問題には非常に効率的な(線形時間)ソリューションがありますが、少し議論が必要です...
ゼロス:問題の明確化/ LCP
コメントの説明によると、@ FooBarは元の問題は要素ごとにmin
であると述べています。次のようなz
(またはv
)を見つける必要があります。
左の引数がゼロで右の引数が非負であるか、左の引数が非負で右の引数がゼロである
@FooBarが正しく指摘しているように、有効な再パラメーター化はLCPにつながります。ただし、以下では、LCPを必要とせずに、元の問題に対するより簡単で効率的な解決策を(元の問題の構造を利用することによって)達成できることを示します。なぜこれが簡単なのですか?さて、LCPにはz
(Bz + q) 'zに二次項がありますが、元の問題にはないことに注意してください(線形項Bz + qとzのみ)。以下でその事実を利用します。
最初:単純化
この問題を大幅に簡素化する重要ですが重要な詳細があります。
- Scipy.sparse.diagsを使用して、4つの行列A11、A12、A21、A22を作成します
- そして、それらをA = scipy.sparse.bmat([[A11、A12]、[A21、A22]])としてスタックします。
これには大きな影響があります。具体的には、これはsinglelargeの問題ではなく、large number of really small(2D、to正確に)問題。このA
行列のブロック対角構造は、後続のすべての操作を通じて保持されることに注意してください。そして、その後のすべての演算は、行列-ベクトル乗算または内積です。これは、実際にはこのプログラムがz
(またはv
)変数のペアでseparableであることを意味します。
具体的には、各ブロックA11,...
のサイズがn
×n
であるとします。次に、z_1
とz_{n+1}
が方程式と項でのみ表示され、他の場所では表示されないことに注意してください。したがって、問題はn
問題に分離可能であり、各問題は2次元です。したがって、今後2D問題を解決し、疎行列や大きなoptパッケージを使用せずに、適切と思われるn
を介してシリアル化または並列化できます。
2番目:2D問題のジオメトリ
2Dに要素ごとの問題があると仮定します。つまり、次のようになります。
find z such that (elementwise) min( Bz + q , z ) = 0, or declare that no such `z` exists.
セットアップではB
が2x2になっているため、この問題のジオメトリは、手動で列挙できる4つのスカラー不等式に対応します(私はそれらにa1、a2、z1、z2という名前を付けました)。
“a1”: B11*z1 + B12*z2 + q1 >=0
“a2”: B21*z1 + B22*z2 + q2 >=0
“z1”: z1 >= 0
“z2:” z2 >= 0
これは、(おそらく空の)多面体、つまり2次元空間の4つの半空間の交点を表します。
番目:2D問題の解決
(編集:わかりやすくするためにこのビットを更新しました)
では、2Dの問題は具体的に何ですか?次の解決策のいずれかが達成されるz
を見つけたいと思います(すべてが異なるわけではありませんが、それは問題ではありません)。
これらのいずれかが達成された場合、解決策があります。zおよびBz + qの要素ごとの最小値が0ベクトルであるz。 4つのうちどれも達成できない場合、問題は実行不可能であり、そのようなz
は存在しないと宣言します。
最初のケースは、a1、a2の交点が正の象限にある場合に発生します。解z = B ^ -1qを選択し、それが要素ごとに非負であるかどうかを確認するだけです。そうである場合は、B^-1q
を返します(Bはpsdではありませんが、反転可能/フルランクであると想定していることに注意してください)。そう:
if B^-1q is elementwise nonnegative, return z = B^-1q.
2番目のケース(最初のケースと完全に区別されるわけではありません)は、a1、a2の交点がどこかにあるが、原点が含まれている場合に発生します。言い換えると、z = 0に対してBz + q> 0の場合は常に。これは、qが要素的に正の場合に発生します。そう:
Elif q is elementwise nonnegative, return z = 0.
3番目のケースにはz1 = 0があり、これをa2に代入して、z2 = -q2/B22のときにa2 = 0であることを示すことができます。 z2は> = 0でなければならないため、-q2/B22> = 0です。 a1も> = 0でなければならず、これをz1とz2の値に代入すると、-B22/B12 * q2 + q1> = 0になります。そう:
Elif -q2/B22 >=0 and -B22/B12*q2 + q1 >=0, return z1= 0, z2 = -q2/B22.
4番目のステップは3番目のステップと同じですが、1と2を入れ替えます。
Elif -q1/B11 >=0 and -B21/B11*q1 + q2 >=0, return z1 = -q1/B11, z2 =0.
最後の5番目のケースは、問題が実行不可能な場合です。これは、上記の条件のいずれも満たされない場合に発生します。そう:
else return None
最後に:ピースをまとめる
各2D問題の解決は、いくつかの単純/効率的/自明な線形解決と比較です。それぞれが数値のペアまたはNone
を返します。次に、すべてのn
2D問題に対して同じことを行い、ベクトルを連結します。いずれかが「なし」の場合、問題は実行不可能です(すべてなし)。そうでなければ、あなたはあなたの答えを持っています。
@denfromufaが指摘したように、AMPL
ソルバーへのPATH
インターフェースがあります。彼はPyomo
を提案しました。これはオープンソースであり、AMPL
を処理できるからです。ただし、Pyomo
は処理が遅く、面倒であることが判明しました。私は最終的にcythonのPATH
ソルバーへの独自のインターフェースを作成し、いつかそれをリリースしたいと思っていますが、現時点では入力の衛生状態がなく、迅速で汚れており、それに取り組んでいます。
今のところ、AMPL
のpython拡張子を使用する答えを共有できます。PATH
への直接インターフェースほど高速ではありません。すべてのLCP
解決したいのですが、(一時的な)モデルファイルを作成し、AMPL
を実行して、出力を収集します。やや速くて汚いですが、少なくとも結果の一部を報告する必要があると感じました。この質問をしてからの過去数ヶ月の。
import os
# PATH license string needs to be updated
os.environ['PATH_LICENSE_STRING'] = "3413119131&Courtesy&&&USR&54784&12_1_2016&1000&PATH&GEN&31_12_2017&0_0_0&5000&0_0"
from amplpy import AMPL, Environment, dataframe
import numpy as np
from scipy import sparse
from tempfile import mkstemp
import os
import sys
import contextlib
class DummyFile(object):
def write(self, x): pass
@contextlib.contextmanager
def nostdout():
save_stdout = sys.stdout
sys.stdout = DummyFile()
yield
sys.stdout = save_stdout
class modFile:
# context manager: create temporary file, insert model data, and supply file name
# apparently, amplpy coders are inable to support StringIO
content = """
set Rn;
param B {Rn,Rn} default 0;
param q {Rn} default 0;
var x {j in Rn};
s.t. f {i in Rn}:
0 <= x[i]
complements
sum {j in Rn} B[i,j]*x[j]
>= -q[i];
"""
def __init__(self):
self.fd = None
self.temp_path = None
def __enter__(self):
fd, temp_path = mkstemp()
file = open(temp_path, 'r+')
file.write(self.content)
file.close()
self.fd = fd
self.temp_path = temp_path
return self
def __exit__(self, exc_type, exc_val, exc_tb):
os.close(self.fd)
os.remove(self.temp_path)
def solveLCP(B, q, x=None, env=None, binaryDirectory=None, pathOptions={'logfile':'logpath.tmp' }, verbose=False):
# x: initial guess
if binaryDirectory is not None:
env = Environment(binaryDirectory='/home/foo/amplide.linux64/')
if verbose:
pathOptions['output'] = 'yes'
ampl = AMPL(environment=env)
# read model
with modFile() as mod:
ampl.read(mod.temp_path)
n = len(q)
# prepare dataframes
dfQ = dataframe.DataFrame('Rn', 'c')
for i in np.arange(n):
# shitty amplpy does not support np.float
dfQ.addRow(int(i)+1, np.float(q[i]))
dfB = dataframe.DataFrame(('RnRow', 'RnCol'), 'val')
if sparse.issparse(B):
if not isinstance(B, sparse.lil_matrix):
B = B.tolil()
dfB.setValues({
(i+1, j+1): B.data[i][jPos]
for i, row in enumerate(B.rows)
for jPos, j in enumerate(row)
})
else:
r = np.arange(n) + 1
Rrow, Rcol = np.meshgrid(r, r, indexing='ij')
#dfB.setColumn('RnRow', [np.float(x) for x in Rrow.reshape((-1), order='C')])
dfB.setColumn('RnRow', list(Rrow.reshape((-1), order='C').astype(float)))
dfB.setColumn('RnCol', list(Rcol.reshape((-1), order='C').astype(float)))
dfB.setColumn('val', list(B.reshape((-1), order='C').astype(float)))
# set values
ampl.getSet('Rn').setValues([int(x) for x in np.arange(n, dtype=int)+1])
if x is not None:
dfX = dataframe.DataFrame('Rn', 'x')
for i in np.arange(n):
# shitty amplpy does not support np.float
dfX.addRow(int(i)+1, np.float(x[i]))
ampl.getVariable('x').setValues(dfX)
ampl.getParameter('q').setValues(dfQ)
ampl.getParameter('B').setValues(dfB)
# solve
ampl.setOption('solver', 'pathampl')
pathOptions = ['{}={}'.format(key, val) for key, val in pathOptions.items()]
ampl.setOption('path_options', ' '.join(pathOptions))
if verbose:
ampl.solve()
else:
with nostdout():
ampl.solve()
if False:
bD = ampl.getParameter('B').getValues().toDict()
qD = ampl.getParameter('q').getValues().toDict()
xD = ampl.getVariable('x').getValues().toDict()
BB = ampl.getParameter('B').getValues().toPandas().values.reshape((n, n,), order='C')
qq = ampl.getParameter('q').getValues().toPandas().values[:, 0]
xx = ampl.getVariable('x').getValues().toPandas().values[:, 0]
ineq2 = BB.dot(xx) + qq
print((xx * ineq2).min(), (xx * ineq2).max() )
return ampl.getVariable('x').getValues().toPandas().values[:, 0]
if __== '__main__':
# solve problem from the Julia port at https://github.com/chkwon/PATHSolver.jl
n = 4
B = np.array([[0, 0, -1, -1], [0, 0, 1, -2], [1, -1, 2, -2], [1, 2, -2, 4]])
q = np.array([2, 2, -2, -6])
BSparse = sparse.lil_matrix(B)
env = Environment(binaryDirectory='/home/foo/amplide.linux64/')
print(solveLCP(B, q, env=env))
print(solveLCP(BSparse, q, env=env))
# to test licensing
from numpy import random
n = 1000
B = np.diag((random.randn(n)))
q = np.ones((n,))
print(solveLCP(B, q, env=env))