/
expopt.py
254 lines (206 loc) · 7.69 KB
/
expopt.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# -*- coding: utf-8 -*-
"""Module for using the MOSEK EXPOPT C interface
Example
-------
``result = _mosek.expopt.imize(cs, A, p_idxs)``
Raises
------
ImportError
If the local MOSEK library could not be loaded
"""
from __future__ import unicode_literals, print_function
from os import path, sep
from ctypes import pointer as ptr
from ctypes import c_double, c_int, CFUNCTYPE
from .baked_ctypesgen import load_library, String, c_void, POINTER, UNCHECKED
from .. import settings
class ModuleShortener(object):
"""Makes ctype calls look like C calls, but still use namespaces.
example in C: MSK_makeemptytask
regular python: MSK.MSK_makeemptytask
w/ModuleShortener: MSK._makeemptytask
Arguments
---------
stub : str
String to append to all getattrs (the string "MSK" above)
module : str
Module to be shortened (the first "MSK" object above)
"""
def __init__(self, stub, module):
self.module = module
self.stub = stub
def __getattr__(self, attribute):
"""Appends stub to all getattr calls
Args
----
attribute : str
Shortened stubless attribute name
Returns
-------
attribute from self.module
"""
return getattr(self.module, self.stub + attribute)
# below is MSKsolsta_enum from mosek.h
# positions changed as noted because MOSEK solves the dual GP problem
MSK_SOL_STA_LOOKUPTABLE = ["UNKNOWN",
"OPTIMAL",
"DUAL_FEAS", # originally position 3
"PRIM_FEAS", # originally position 2
"PRIM_AND_DUAL_FEAS",
"DUAL_INFEAS_CER", # originally position 6
"PRIM_INFEAS_CER", # originally position 5
"NEAR_OPTIMAL",
"NEAR_DUAL_FEAS", # originally position 9
"NEAR_PRIM_FEAS", # originally position 8
"NEAR_PRIM_AND_DUAL_FEAS",
"NEAR_DUAL_INFEAS_CER", # originally position 12
"NEAR_PRIM_INFEAS_CER", # originally position 11
"INTEGER_OPTIMAL",
"NEAR_INTEGER_OPTIMAL"]
def c_array(py_array, c_type):
"""Makes a C array from a python list or array and a C datatype
Arguments
----------
py_array: array-like data to convert
c_type: C datatype to which elements of py_array will be converted
Returns
-------
C array of chosen datatype
"""
if not isinstance(py_array, list):
pya = list(py_array)
else:
pya = py_array
return (c_type * len(pya))(*pya)
LIB = path.abspath(__file__).replace("expopt.py", "build%sexpopt.so" % sep)
MSK = ModuleShortener("MSK", load_library(LIB))
MSK_RES_OK = 0
MSK_IPAR_INTPNT_MAX_ITERATIONS = 19
MSKuserhandle_t = POINTER(c_void)
MSKstreamfunc = CFUNCTYPE(UNCHECKED(None), MSKuserhandle_t, String)
@MSKstreamfunc
def printcb(_, msg):
"""Function to handle MOSEK's internal logging
To enable printing to the python console, add a line like
`print msg[:-1]`
before the return statement.
Arguments
----------
void : None
Placeholder to emulate C function
msg : C string
One particular log message; since it's a C string the last byte is null.
Returns
-------
result : int
0 indicates success
"""
print(msg[:-1])
return 0
# pylint: disable=unused-argument,too-many-locals,protected-access
def imize(c, A, p_idxs, *args, **kwargs):
"""Interface to the MOSEK EXPOPT solver via C
This code is based on the example C file "tskexpopt.c" at
"[...]/mosek/7/tools/examples/c/tstexpopt.c"
Definitions
-----------
"[a,b] array of floats" indicates array-like data with shape [a,b]
n is the number of monomials in the gp
m is the number of variables in the gp
p is the number of posynomials in the gp
Arguments
----------
c : floats array of shape n
Coefficients of each monomial
A: floats array of shape (m,n)
Exponents of the various free variables for each monomial.
p_idxs: ints array of shape n
Posynomial index of each monomial
Returns
-------
dict
Contains the following keys
"success": bool
"objective_sol" float
Optimal value of the objective
"primal_sol": floats array of size m
Optimal value of the free variables. Note: not in logspace.
"dual_sol": floats array of size p
Optimal value of the dual variables, in logspace.
Raises
------
None, but because it calls C code you can't Ctrl-C out of it easily. :-/
"""
r = MSK_RES_OK
numcon = 1+p_idxs[-1]
numter, numvar = map(int, A.shape)
xx = c_array([0]*numvar, c_double)
yy = c_array([0]*numter, c_double)
numcon, numvar, numter = map(c_int, [numcon, numvar, numter])
c = c_array(c, c_double)
subi = c_array(p_idxs, c_int)
subk = c_array(A.row, c_int)
subj = c_array(A.col, c_int)
akj = c_array(A.data, c_double)
numanz = c_int(len(A.data))
objval = c_double()
env = POINTER(c_void)()
prosta = c_int()
solsta = c_int()
expopttask = POINTER(c_void)()
expopthnd = POINTER(c_void)()
# a little extra work to declare a pointer for expopthnd...
ptr_expopthnd = POINTER(POINTER(c_void))(expopthnd)
if r == MSK_RES_OK:
r = MSK._makeenv(ptr(env), None)
if r == MSK_RES_OK:
r = MSK._makeemptytask(env, ptr(expopttask))
if r == MSK_RES_OK:
r = MSK._linkfunctotaskstream(expopttask, 0, None, printcb)
if r == MSK_RES_OK:
# Initialize expopttask with problem data
r = MSK._expoptsetup(expopttask,
c_int(1), # Solve the dual formulation
numcon,
numvar,
numter,
subi,
c,
subk,
subj,
akj,
numanz,
# Pointer to data structure holding nonlinear data
ptr_expopthnd
)
# Any parameter can now be changed with standard mosek function calls
if r == MSK_RES_OK:
r = MSK._putintparam(expopttask,
MSK_IPAR_INTPNT_MAX_ITERATIONS,
c_int(200))
# Optimize, xx holds the primal optimal solution,
# yy holds solution to the dual problem
if r == MSK_RES_OK:
r = MSK._expoptimize(expopttask,
ptr(prosta),
ptr(solsta),
ptr(objval),
ptr(xx),
ptr(yy),
ptr_expopthnd)
# Free data allocated by expoptsetup
if ptr_expopthnd:
MSK._expoptfree(expopttask,
ptr_expopthnd)
MSK._deletetask(ptr(expopttask))
MSK._deleteenv(ptr(env))
status = MSK_SOL_STA_LOOKUPTABLE[solsta.value] # pylint:disable=invalid-sequence-index
# Allow mosek's NEAR_DUAL_FEAS solution status, because our check in gp.py
# will catch solutions that don't actually meet our tolerance
# TODO: when we standardize solver status responses, revisit this.
if status == "NEAR_DUAL_FEAS":
status = "OPTIMAL"
return dict(status=status,
objective=objval.value,
primal=list(xx),
nu=list(yy))