引言
近期一个工作中使用Java进行编程,涉及到一个数学规划/优化问题的求解,本想着通过Java调用Gurobi来求解,但约束中含有指数函数,在尝试后发现Gurobi求解速度较慢,并且在问题规模增大后求解过程没有终止的迹象。
后来发现Matlab中的CVX适合求解这样的问题,最终决定通过Java调用Matlab来进行求解。主要的方案有两种:一是将Matlab中的.m文件打包成JAR包,再导入到Java工程中调用;二是在Java中创建MatlabEngine对象来执行Matlab程序。这两种方案我都进行了尝试,第一种方案没有走通,第二种方案走通了,在此记录一下。
方案一
首先我尝试了方案一,打包和导入过程网上都有较多案例,我也顺利地完成了。之后耗费时间较长的一个问题是函数的调用。后来发现这种打包会生成详细的说明文档(在打包生成的doc文件夹中可以找到),如下图所示。
以RS_KL_max为例,RS_KL_max是我在Matlab中写的一个函数,JAR包里针对这一个函数生成了三个方法,目前我还不太明白后两个方法的作用,根据说明第一个方法是“标准接口”,大多数情况下应该是使用这一方法,点击方法名可以跳转到详细说明,如下(图中的乱码在Matlab中是中文,应该是打包过程存在编码问题,可忽略)。
这一说明非常详细(写这篇博文的时候才注意到有这个文档,emmm,提示有时先不要急着在网上找解答,尽量看看有没有官方的说明),有下面几个关键点。一是第一个参数是返回参数的个数,这个等于是额外出现的一个参数(我在Matlab中写的RS_KL_max函数是三个输入参数,一个输出参数),需要根据实际情况设置;二是提到输入参数应该由3个逗号分隔(3 comma-seperated),3个逗号正好是4个参数,等于是原本的3个加上表示返回参数个数的那一个;三是提到了输入参数的类型;四是提到了返回值的类型。
public static void Java_cvx_1(ArrayList<Double> H_list,double eta) throws MWException{
// 有可能cvx的代码并未编译
int n = H_list.size();
int[] dim = {1,n};
double[] h = new double[n];
for (int i = 0; i < n; i++) {
h[i] = H_list.get(i);
}
Object[] x = {h};
Object[] outputs = null;
RS_KL_cvx RS = null;
MWNumericArray y = new MWNumericArray(x,MWClassID.DOUBLE);
MWNumericArray mwn = new MWNumericArray(n,MWClassID.INT32);
MWNumericArray mweta = new MWNumericArray(eta,MWClassID.DOUBLE);
// for (int i = 0; i < n; i++){
// x[i] = new MWNumericArray(Double.valueOf(H_list.get(i)), MWClassID.DOUBLE);
// }
//MWNumericArray H = MWNumericArray.newInstance(dim,h,MWClassID.DOUBLE);
RS = new RS_KL_cvx();
outputs = RS.RS_KL_max(1,mwn,y,mweta);
System.out.println(outputs[0].toString());
}
我的代码如上所示,运行后会出现下面的错误。由于报错中出现了cvx,我推测方法是正常调用了,参数传递正常,但在方法内调用cvx时出现了问题。在网上搜索“PostVMInit failed to initialize com.mathworks.mwswing.MJStartup”和“此类型的变量不支持使用点进行索引”,没太看到非常相关的内容。发现论坛上一个提问是说想把simulink的某部分打包成JAR报这个错误,下面有员工回答simulink不支持这样打包,并表示这个报错提示得不明确。想一想也有道理,如果Matlab所有部分都能这样打包,那它作为付费软件不就没有意义了……又注意到之前看到的成功的例子都没有涉及调用其他toolbox,综合下来推测可能是cvx不支持这样的打包。
PostVMInit failed to initialize com.mathworks.mwswing.MJStartup
此类型的变量不支持使用点进行索引。
出错 cvx_global (第 76 行)
出错 cvxprob (第 4 行)
出错 cvx_begin (第 41 行)
出错 RS_KL_max (第 4 行)
Exception in thread "main" ... Matlab M-code Stack Trace ...
file C:\...\RS_KL_0\Software\cvx\cvx-w64\cvx\lib\cvx_global.m, name cvx_global, line 76.
file C:\...\RS_KL_0\Software\cvx\cvx-w64\cvx\lib\@cvxprob\cvxprob.m, name cvxprob, line 4.
file C:\...\RS_KL_0\Software\cvx\cvx-w64\cvx\commands\cvx_begin.m, name cvx_begin, line 41.
file C:\...\RS_KL_0\RS_KL_cvx\RS_KL_max.m, name RS_KL_max, line 4.
方案二
方案二是在Java中启动一个Matlab engine,之后就可以和Matlab进行交互,现在来看,这个engine应该是一个运行Matlab终端的子进程(这一思路和Java与GAMS交互非常类似,参考GAMS相关的博文GAMS使用小记(二))。网上相关的教程也比较多,但最后一步还是自己摸索出一个有些生硬但有效的方法解决了问题。相关代码如下。
public static void Java_cvx_2(ArrayList<Double> H_list,double eta) throws MWException{
int n = H_list.size();
int[] dim = {1,n};
double[] h = new double[n];
String h_s = new String("[");
for (int i = 0; i < n; i++) {
h_s = h_s + H_list.get(i) + ",";
}
h_s = h_s + "]";
try {
// 启动Matlab引擎
MatlabEngine eng = MatlabEngine.startMatlab();
eng.eval("cd D:\\Programming\\Matlab\\");
//double upper= eng.feval("add",n*1.0,eta); 可以调用
//eng.eval("cvx_setup");
eng.eval("RS_KL_max("+n+","+eta+","+h_s+")");
//eng.feval("yourFunction", param1, param2, ...);
// 添加Matlab函数所在的路径
//eng.feval("D:\\Programming\\Matlab");
// 调用Matlab函数
//double upper= eng.feval("RS_KL_max",n,eta,h);//需指定返回值数量,默认为1
//System.out.println(upper);
//Object[] Z = eng.getVariable("Z");//获得输出值
// 关闭Matlab引擎
eng.close();
} catch (Exception e) {
e.printStackTrace();
}
}
首先碰到的一个问题是出现“MatlabConversionError”这一错误,并且提示出错代码是“MatlabEngine eng = MatlabEngine.startMatlab();”这一句,等于是Matlab无法正常启动。在网上搜索会发现少有匹配的内容,说明这一错误不太常见(可能每个人遇到的错误点都不太一样……),最常提到的是要按照官网的步骤添加环境变量,我也添加了,有些奇怪的时候看到一个回答说“重启让环境变量生效”,突然意识到就像在terminal里写命令一样,更新环境变量后需要重新开一个terminal来验证。于是重启了IDEA,果然MatlabEngine可以正常启动了。
随后遇到了新的问题,我在网上以及官方的文档中都看到用feval来执行有返回值的命令(eval执行没有返回值的命令),于是我使用了上面注释掉的“double upper= eng.feval(“RS_KL_max”,n,eta,h);”想执行这一函数,会出现下面的报错,非常奇怪的是有很多小字,仔细看是很多“SUB”,不知道是什么情况。
正当有些沮丧时,我意识到eval这个函数其实就相当于在Matlab终端输入命令,那只要是Matlab终端能执行的,在Java中一样可以把这条命令拼接出来,于是根据Matlab中的调用方式(如:RS_KL_max(6,1.5,[2.6,2.7,2.8,2.9,3.0])),有了下面的代码。
String h_s = new String("[");
for (int i = 0; i < n; i++) {
h_s = h_s + H_list.get(i) + ",";
}
h_s = h_s + "]";
eng.eval("RS_KL_max("+n+","+eta+","+h_s+")");
成功了!以往总觉得这样拼接命令有些生硬,现在看来这是一种很直接有效的方法。进一步如果想获得返回值怎么做?其实关键点在于完全当成在终端执行命令,稍微修改上面的代码即可在Java中获得返回值。
String h_s = new String("[");
for (int i = 0; i < n; i++) {
h_s = h_s + H_list.get(i) + ",";
}
h_s = h_s + "]";
eng.eval("Z=RS_KL_max("+n+","+eta+","+h_s+")");//将返回值赋给变量Z
double Z = eng.getVariable("Z");//获得返回值
System.out.println(Z);
感想
- 混合编程一般是不得已的选择,但有时也能拓宽思路,充分利用不同语言/软件的优点。
- 官网的文档和论坛是质量很高的。
- 类似问题的解决方案往往是有些类似的,争取触类旁通。