passagemath-highs 10.8.1rc3__cp311-cp311-win_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- passagemath_highs/__init__.py +3 -0
- passagemath_highs-10.8.1rc3.dist-info/DELVEWHEEL +2 -0
- passagemath_highs-10.8.1rc3.dist-info/METADATA +98 -0
- passagemath_highs-10.8.1rc3.dist-info/RECORD +18 -0
- passagemath_highs-10.8.1rc3.dist-info/WHEEL +5 -0
- passagemath_highs-10.8.1rc3.dist-info/top_level.txt +3 -0
- passagemath_highs.libs/libc++-ed3f98626a61fdd3abff58323519007d.dll +0 -0
- passagemath_highs.libs/libhighs-025266193de3fbea5edd48a6000281c8.dll +0 -0
- passagemath_highs.libs/zlib1-bad03dcdb8eadc9f49b21b64350515f3.dll +0 -0
- sage/all__sagemath_highs.py +11 -0
- sage/libs/all__sagemath_highs.py +1 -0
- sage/libs/highs/__init__.py +2 -0
- sage/libs/highs/highs_c_api.pxd +200 -0
- sage/numerical/all__sagemath_highs.py +1 -0
- sage/numerical/backends/all__sagemath_highs.py +1 -0
- sage/numerical/backends/highs_backend.cp311-win_arm64.pyd +0 -0
- sage/numerical/backends/highs_backend.pxd +34 -0
- sage/numerical/backends/highs_backend.pyx +2699 -0
|
@@ -0,0 +1,2699 @@
|
|
|
1
|
+
# sage_setup: distribution = sagemath-highs
|
|
2
|
+
"""
|
|
3
|
+
HiGHS Backend
|
|
4
|
+
|
|
5
|
+
AUTHORS:
|
|
6
|
+
|
|
7
|
+
- Chenxin Zhong (chenxin.zhong@outlook.com): initial implementation
|
|
8
|
+
|
|
9
|
+
This backend uses the HiGHS optimization solver C API, which supports Linear Programming (LP),
|
|
10
|
+
Quadratic Programming (QP), and Mixed Integer Programming (MIP).
|
|
11
|
+
|
|
12
|
+
HiGHS is available under the MIT License.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
# ****************************************************************************
|
|
16
|
+
# Copyright (C) 2025 SageMath Developers
|
|
17
|
+
#
|
|
18
|
+
# This program is free software: you can redistribute it and/or modify
|
|
19
|
+
# it under the terms of the GNU General Public License as published by
|
|
20
|
+
# the Free Software Foundation, either version 2 of the License, or
|
|
21
|
+
# (at your option) any later version.
|
|
22
|
+
# https://www.gnu.org/licenses/
|
|
23
|
+
# ****************************************************************************
|
|
24
|
+
|
|
25
|
+
from sage.numerical.mip import MIPSolverException
|
|
26
|
+
from copy import copy
|
|
27
|
+
from cysignals.signals cimport sig_on, sig_off
|
|
28
|
+
|
|
29
|
+
from sage.libs.highs.highs_c_api cimport *
|
|
30
|
+
|
|
31
|
+
# C standard library for memory allocation
|
|
32
|
+
cdef extern from "stdlib.h":
|
|
33
|
+
void* malloc(size_t size) nogil
|
|
34
|
+
void free(void* ptr) nogil
|
|
35
|
+
|
|
36
|
+
cdef class HiGHSBackend(GenericBackend):
|
|
37
|
+
"""
|
|
38
|
+
MIP Backend that uses the HiGHS solver via C API.
|
|
39
|
+
|
|
40
|
+
HiGHS is a high-performance solver for large-scale LP, QP, and MIP.
|
|
41
|
+
This implementation uses the HiGHS C API directly for optimal performance
|
|
42
|
+
and proper interrupt handling with sig_on/sig_off.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __cinit__(self, maximization=True):
|
|
46
|
+
"""
|
|
47
|
+
Constructor.
|
|
48
|
+
|
|
49
|
+
EXAMPLES::
|
|
50
|
+
|
|
51
|
+
sage: p = MixedIntegerLinearProgram(solver='HiGHS')
|
|
52
|
+
"""
|
|
53
|
+
# Create HiGHS instance
|
|
54
|
+
self.highs = Highs_create()
|
|
55
|
+
if self.highs == NULL:
|
|
56
|
+
raise MemoryError("Failed to create HiGHS instance")
|
|
57
|
+
|
|
58
|
+
# Initialize metadata
|
|
59
|
+
self.prob_name = ""
|
|
60
|
+
self.col_name_var = {}
|
|
61
|
+
self.row_name_var = {}
|
|
62
|
+
self.row_data_cache = {}
|
|
63
|
+
self.numcols = 0
|
|
64
|
+
self.numrows = 0
|
|
65
|
+
self.obj_constant_term = 0.0
|
|
66
|
+
|
|
67
|
+
# Suppress HiGHS output messages
|
|
68
|
+
# output_flag is the master switch for all HiGHS output
|
|
69
|
+
# log_to_console controls whether log messages go to console
|
|
70
|
+
Highs_setBoolOptionValue(self.highs, b"output_flag", 0)
|
|
71
|
+
Highs_setBoolOptionValue(self.highs, b"log_to_console", 0)
|
|
72
|
+
|
|
73
|
+
# Set tighter MIP feasibility tolerance to avoid floating-point imprecision
|
|
74
|
+
# in objective values (default 1e-6 can cause values like 1.999999999999985
|
|
75
|
+
# instead of 2.0). See https://github.com/sagemath/sage/pull/41105
|
|
76
|
+
Highs_setDoubleOptionValue(self.highs, b"mip_feasibility_tolerance", 1e-7)
|
|
77
|
+
|
|
78
|
+
# Disable MIP symmetry detection to work around a known HiGHS bug
|
|
79
|
+
# (https://github.com/ERGO-Code/HiGHS/issues/1670) where the parallel
|
|
80
|
+
# task executor can deadlock during symmetry detection. The bug is in
|
|
81
|
+
# HiGHS's work-stealing scheduler: when finishSymmetryDetection() calls
|
|
82
|
+
# taskGroup.sync(), a race condition in HighsSplitDeque::waitForTaskToFinish()
|
|
83
|
+
# can cause the notification from the finishing task to be lost, resulting
|
|
84
|
+
# in an indefinite pthread_cond_wait. This affects all thread settings
|
|
85
|
+
# including threads=1, suggesting the issue may also involve interactions
|
|
86
|
+
# with cysignals' signal handling. The trade-off is that MIP problems won't
|
|
87
|
+
# benefit from symmetry exploitation, but this is preferable to hanging.
|
|
88
|
+
Highs_setBoolOptionValue(self.highs, b"mip_detect_symmetry", 0)
|
|
89
|
+
|
|
90
|
+
# Set optimization sense
|
|
91
|
+
if maximization:
|
|
92
|
+
self.set_sense(+1)
|
|
93
|
+
else:
|
|
94
|
+
self.set_sense(-1)
|
|
95
|
+
|
|
96
|
+
def __dealloc__(self):
|
|
97
|
+
"""
|
|
98
|
+
Destructor - free HiGHS instance.
|
|
99
|
+
"""
|
|
100
|
+
if self.highs != NULL:
|
|
101
|
+
Highs_destroy(self.highs)
|
|
102
|
+
self.highs = NULL
|
|
103
|
+
|
|
104
|
+
cdef void _get_col_bounds(self, int col, double* lb, double* ub) except *:
|
|
105
|
+
"""
|
|
106
|
+
Helper method to get column bounds using Highs_getColsByRange.
|
|
107
|
+
"""
|
|
108
|
+
cdef HighsInt num_col, num_nz, status
|
|
109
|
+
|
|
110
|
+
sig_on()
|
|
111
|
+
status = Highs_getColsByRange(self.highs, col, col,
|
|
112
|
+
&num_col, NULL, lb, ub,
|
|
113
|
+
&num_nz, NULL, NULL, NULL)
|
|
114
|
+
sig_off()
|
|
115
|
+
|
|
116
|
+
if status != kHighsStatusOk:
|
|
117
|
+
raise MIPSolverException("HiGHS: Failed to get column bounds")
|
|
118
|
+
|
|
119
|
+
cdef void _get_row_bounds(self, int row, double* lb, double* ub) except *:
|
|
120
|
+
"""
|
|
121
|
+
Helper method to get row bounds using Highs_getRowsByRange.
|
|
122
|
+
"""
|
|
123
|
+
cdef HighsInt num_row, num_nz, status
|
|
124
|
+
|
|
125
|
+
sig_on()
|
|
126
|
+
status = Highs_getRowsByRange(self.highs, row, row,
|
|
127
|
+
&num_row, lb, ub,
|
|
128
|
+
&num_nz, NULL, NULL, NULL)
|
|
129
|
+
sig_off()
|
|
130
|
+
|
|
131
|
+
if status != kHighsStatusOk:
|
|
132
|
+
raise MIPSolverException("HiGHS: Failed to get row bounds")
|
|
133
|
+
|
|
134
|
+
cpdef int add_variable(self, lower_bound=0.0, upper_bound=None, binary=False,
|
|
135
|
+
continuous=False, integer=False, obj=0.0, name=None) except -1:
|
|
136
|
+
"""
|
|
137
|
+
Add a variable.
|
|
138
|
+
|
|
139
|
+
This amounts to adding a new column to the matrix. By default,
|
|
140
|
+
the variable is both positive, real and the coefficient in the
|
|
141
|
+
objective function is 0.0.
|
|
142
|
+
|
|
143
|
+
INPUT:
|
|
144
|
+
|
|
145
|
+
- ``lower_bound`` -- the lower bound of the variable (default: 0)
|
|
146
|
+
|
|
147
|
+
- ``upper_bound`` -- the upper bound of the variable (default: ``None``)
|
|
148
|
+
|
|
149
|
+
- ``binary`` -- ``True`` if the variable is binary (default: ``False``)
|
|
150
|
+
|
|
151
|
+
- ``continuous`` -- ``True`` if the variable is continuous (default: ``True``)
|
|
152
|
+
|
|
153
|
+
- ``integer`` -- ``True`` if the variable is integral (default: ``False``)
|
|
154
|
+
|
|
155
|
+
- ``obj`` -- (optional) coefficient of this variable in the objective function (default: 0.0)
|
|
156
|
+
|
|
157
|
+
- ``name`` -- an optional name for the newly added variable (default: ``None``)
|
|
158
|
+
|
|
159
|
+
OUTPUT: the index of the newly created variable
|
|
160
|
+
|
|
161
|
+
EXAMPLES::
|
|
162
|
+
|
|
163
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
164
|
+
sage: p = get_solver(solver = "HiGHS")
|
|
165
|
+
sage: p.ncols()
|
|
166
|
+
0
|
|
167
|
+
sage: p.add_variable()
|
|
168
|
+
0
|
|
169
|
+
sage: p.ncols()
|
|
170
|
+
1
|
|
171
|
+
sage: p.add_variable(binary=True)
|
|
172
|
+
1
|
|
173
|
+
sage: p.add_variable(lower_bound=-2.0, integer=True)
|
|
174
|
+
2
|
|
175
|
+
sage: p.add_variable(continuous=True, integer=True)
|
|
176
|
+
Traceback (most recent call last):
|
|
177
|
+
...
|
|
178
|
+
ValueError: ...
|
|
179
|
+
sage: p.add_variable(name='x', obj=1.0)
|
|
180
|
+
3
|
|
181
|
+
sage: p.col_name(3)
|
|
182
|
+
'x'
|
|
183
|
+
sage: p.objective_coefficient(3)
|
|
184
|
+
1.0
|
|
185
|
+
"""
|
|
186
|
+
cdef HighsInt var_type
|
|
187
|
+
cdef double lb, ub
|
|
188
|
+
cdef HighsInt status
|
|
189
|
+
cdef HighsInt col_idx
|
|
190
|
+
|
|
191
|
+
# Determine variable type - only one of binary, continuous, integer can be True
|
|
192
|
+
if sum([binary, continuous, integer]) > 1:
|
|
193
|
+
raise ValueError("only one of binary, continuous, and integer can be True")
|
|
194
|
+
|
|
195
|
+
if binary:
|
|
196
|
+
var_type = kHighsVarTypeInteger
|
|
197
|
+
lb = 0.0
|
|
198
|
+
ub = 1.0
|
|
199
|
+
elif integer:
|
|
200
|
+
var_type = kHighsVarTypeInteger
|
|
201
|
+
else:
|
|
202
|
+
var_type = kHighsVarTypeContinuous
|
|
203
|
+
|
|
204
|
+
# Set bounds
|
|
205
|
+
if lower_bound is None:
|
|
206
|
+
if binary:
|
|
207
|
+
lb = 0.0
|
|
208
|
+
else:
|
|
209
|
+
lb = -Highs_getInfinity(self.highs)
|
|
210
|
+
else:
|
|
211
|
+
lb = float(lower_bound)
|
|
212
|
+
|
|
213
|
+
if upper_bound is None:
|
|
214
|
+
if binary:
|
|
215
|
+
ub = 1.0
|
|
216
|
+
else:
|
|
217
|
+
ub = Highs_getInfinity(self.highs)
|
|
218
|
+
else:
|
|
219
|
+
ub = float(upper_bound)
|
|
220
|
+
|
|
221
|
+
# Add column with empty constraint coefficients
|
|
222
|
+
sig_on()
|
|
223
|
+
status = Highs_addCol(self.highs, float(obj), lb, ub, 0, NULL, NULL)
|
|
224
|
+
sig_off()
|
|
225
|
+
|
|
226
|
+
if status != kHighsStatusOk:
|
|
227
|
+
raise MIPSolverException("HiGHS: Failed to add variable")
|
|
228
|
+
|
|
229
|
+
col_idx = self.numcols
|
|
230
|
+
self.numcols += 1
|
|
231
|
+
|
|
232
|
+
# Set integrality if needed
|
|
233
|
+
if var_type == kHighsVarTypeInteger:
|
|
234
|
+
sig_on()
|
|
235
|
+
status = Highs_changeColIntegrality(self.highs, col_idx, kHighsVarTypeInteger)
|
|
236
|
+
sig_off()
|
|
237
|
+
if status != kHighsStatusOk:
|
|
238
|
+
raise MIPSolverException("HiGHS: Failed to set variable integrality")
|
|
239
|
+
|
|
240
|
+
# Set name if provided
|
|
241
|
+
if name is not None:
|
|
242
|
+
name_bytes = str(name).encode('utf-8')
|
|
243
|
+
Highs_passColName(self.highs, col_idx, name_bytes)
|
|
244
|
+
self.col_name_var[col_idx] = str(name)
|
|
245
|
+
|
|
246
|
+
return col_idx
|
|
247
|
+
|
|
248
|
+
cpdef int add_variable_with_type(self, int vtype, lower_bound=0.0, upper_bound=None,
|
|
249
|
+
obj=0.0, name=None) except -1:
|
|
250
|
+
"""
|
|
251
|
+
Add a variable with type specified as an integer.
|
|
252
|
+
|
|
253
|
+
This amounts to adding a new column to the matrix. By default,
|
|
254
|
+
the variable is positive and real, and the coefficient in the
|
|
255
|
+
objective function is 0.0.
|
|
256
|
+
|
|
257
|
+
INPUT:
|
|
258
|
+
|
|
259
|
+
- ``vtype`` -- integer specifying the variable type:
|
|
260
|
+
|
|
261
|
+
* ``1`` = Integer
|
|
262
|
+
* ``0`` = Binary
|
|
263
|
+
* ``-1`` = Real (Continuous)
|
|
264
|
+
|
|
265
|
+
- ``lower_bound`` -- the lower bound of the variable (default: 0)
|
|
266
|
+
|
|
267
|
+
- ``upper_bound`` -- the upper bound of the variable (default: ``None``)
|
|
268
|
+
|
|
269
|
+
- ``obj`` -- (optional) coefficient of this variable in the objective function (default: 0.0)
|
|
270
|
+
|
|
271
|
+
- ``name`` -- an optional name for the newly added variable (default: ``None``)
|
|
272
|
+
|
|
273
|
+
OUTPUT: the index of the newly created variable
|
|
274
|
+
|
|
275
|
+
EXAMPLES::
|
|
276
|
+
|
|
277
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
278
|
+
sage: p = get_solver(solver = "HiGHS")
|
|
279
|
+
sage: p.ncols()
|
|
280
|
+
0
|
|
281
|
+
sage: p.add_variable_with_type(-1) # Continuous variable
|
|
282
|
+
0
|
|
283
|
+
sage: p.is_variable_continuous(0)
|
|
284
|
+
True
|
|
285
|
+
sage: p.add_variable_with_type(0) # Binary variable
|
|
286
|
+
1
|
|
287
|
+
sage: p.is_variable_binary(1)
|
|
288
|
+
True
|
|
289
|
+
sage: p.add_variable_with_type(1, lower_bound=-2.0) # Integer variable
|
|
290
|
+
2
|
|
291
|
+
sage: p.is_variable_integer(2)
|
|
292
|
+
True
|
|
293
|
+
sage: p.add_variable_with_type(1, name='x', obj=1.0)
|
|
294
|
+
3
|
|
295
|
+
sage: p.col_name(3)
|
|
296
|
+
'x'
|
|
297
|
+
sage: p.objective_coefficient(3)
|
|
298
|
+
1.0
|
|
299
|
+
|
|
300
|
+
TESTS:
|
|
301
|
+
|
|
302
|
+
Invalid variable type raises an error::
|
|
303
|
+
|
|
304
|
+
sage: p.add_variable_with_type(2)
|
|
305
|
+
Traceback (most recent call last):
|
|
306
|
+
...
|
|
307
|
+
ValueError: Invalid variable type 2. Must be -1 (continuous), 0 (binary), or 1 (integer)
|
|
308
|
+
"""
|
|
309
|
+
cdef HighsInt var_type
|
|
310
|
+
cdef double lb, ub
|
|
311
|
+
cdef HighsInt status
|
|
312
|
+
cdef HighsInt col_idx
|
|
313
|
+
|
|
314
|
+
# Validate and determine variable type
|
|
315
|
+
if vtype == 1:
|
|
316
|
+
# Integer
|
|
317
|
+
var_type = kHighsVarTypeInteger
|
|
318
|
+
elif vtype == 0:
|
|
319
|
+
# Binary - set type to integer with bounds [0,1]
|
|
320
|
+
var_type = kHighsVarTypeInteger
|
|
321
|
+
elif vtype == -1:
|
|
322
|
+
# Continuous
|
|
323
|
+
var_type = kHighsVarTypeContinuous
|
|
324
|
+
else:
|
|
325
|
+
raise ValueError(f"Invalid variable type {vtype}. Must be -1 (continuous), 0 (binary), or 1 (integer)")
|
|
326
|
+
|
|
327
|
+
# Set bounds
|
|
328
|
+
if vtype == 0: # Binary
|
|
329
|
+
# Binary variables have fixed bounds [0, 1]
|
|
330
|
+
lb = 0.0
|
|
331
|
+
ub = 1.0
|
|
332
|
+
# Override user-provided bounds for binary variables
|
|
333
|
+
else:
|
|
334
|
+
# For integer and continuous variables, use provided bounds
|
|
335
|
+
if lower_bound is None:
|
|
336
|
+
lb = -Highs_getInfinity(self.highs)
|
|
337
|
+
else:
|
|
338
|
+
lb = float(lower_bound)
|
|
339
|
+
|
|
340
|
+
if upper_bound is None:
|
|
341
|
+
ub = Highs_getInfinity(self.highs)
|
|
342
|
+
else:
|
|
343
|
+
ub = float(upper_bound)
|
|
344
|
+
|
|
345
|
+
# Add column with empty constraint coefficients
|
|
346
|
+
sig_on()
|
|
347
|
+
status = Highs_addCol(self.highs, float(obj), lb, ub, 0, NULL, NULL)
|
|
348
|
+
sig_off()
|
|
349
|
+
|
|
350
|
+
if status != kHighsStatusOk:
|
|
351
|
+
raise MIPSolverException("HiGHS: Failed to add variable")
|
|
352
|
+
|
|
353
|
+
col_idx = self.numcols
|
|
354
|
+
self.numcols += 1
|
|
355
|
+
|
|
356
|
+
# Set integrality if needed (for integer or binary)
|
|
357
|
+
if var_type == kHighsVarTypeInteger:
|
|
358
|
+
sig_on()
|
|
359
|
+
status = Highs_changeColIntegrality(self.highs, col_idx, kHighsVarTypeInteger)
|
|
360
|
+
sig_off()
|
|
361
|
+
if status != kHighsStatusOk:
|
|
362
|
+
raise MIPSolverException("HiGHS: Failed to set variable integrality")
|
|
363
|
+
|
|
364
|
+
# Set name if provided
|
|
365
|
+
if name is not None:
|
|
366
|
+
name_bytes = str(name).encode('utf-8')
|
|
367
|
+
Highs_passColName(self.highs, col_idx, name_bytes)
|
|
368
|
+
self.col_name_var[col_idx] = str(name)
|
|
369
|
+
|
|
370
|
+
return col_idx
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
cpdef set_sense(self, int sense):
|
|
374
|
+
"""
|
|
375
|
+
Set the direction (maximization/minimization).
|
|
376
|
+
|
|
377
|
+
INPUT:
|
|
378
|
+
|
|
379
|
+
- ``sense`` -- +1 for maximization; any other integer for minimization
|
|
380
|
+
|
|
381
|
+
EXAMPLES::
|
|
382
|
+
|
|
383
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
384
|
+
sage: p = get_solver(solver='HiGHS')
|
|
385
|
+
sage: p.is_maximization()
|
|
386
|
+
True
|
|
387
|
+
sage: p.set_sense(-1)
|
|
388
|
+
sage: p.is_maximization()
|
|
389
|
+
False
|
|
390
|
+
"""
|
|
391
|
+
cdef HighsInt highs_sense
|
|
392
|
+
cdef HighsInt status
|
|
393
|
+
|
|
394
|
+
if sense == 1:
|
|
395
|
+
highs_sense = kHighsObjSenseMaximize
|
|
396
|
+
else:
|
|
397
|
+
highs_sense = kHighsObjSenseMinimize
|
|
398
|
+
|
|
399
|
+
sig_on()
|
|
400
|
+
status = Highs_changeObjectiveSense(self.highs, highs_sense)
|
|
401
|
+
sig_off()
|
|
402
|
+
|
|
403
|
+
if status != kHighsStatusOk:
|
|
404
|
+
raise MIPSolverException("HiGHS: Failed to set objective sense")
|
|
405
|
+
|
|
406
|
+
cpdef int solve(self) except -1:
|
|
407
|
+
"""
|
|
408
|
+
Solve the problem.
|
|
409
|
+
|
|
410
|
+
Sage uses HiGHS's implementation of the branch-and-cut
|
|
411
|
+
algorithm to solve mixed-integer linear programs. HiGHS
|
|
412
|
+
automatically selects the most appropriate algorithm based
|
|
413
|
+
on the problem type.
|
|
414
|
+
|
|
415
|
+
.. NOTE::
|
|
416
|
+
|
|
417
|
+
This method raises ``MIPSolverException`` exceptions when
|
|
418
|
+
the solution cannot be computed for any reason (none
|
|
419
|
+
exists, or the solver was not able to find it, etc...)
|
|
420
|
+
|
|
421
|
+
EXAMPLES::
|
|
422
|
+
|
|
423
|
+
sage: lp = MixedIntegerLinearProgram(solver = 'HiGHS', maximization = False)
|
|
424
|
+
sage: x, y = lp[0], lp[1]
|
|
425
|
+
sage: lp.add_constraint(-2*x + y <= 1)
|
|
426
|
+
sage: lp.add_constraint(x - y <= 1)
|
|
427
|
+
sage: lp.add_constraint(x + y >= 2)
|
|
428
|
+
sage: lp.set_objective(x + y)
|
|
429
|
+
sage: lp.set_integer(x)
|
|
430
|
+
sage: lp.set_integer(y)
|
|
431
|
+
sage: lp.solve()
|
|
432
|
+
2.0
|
|
433
|
+
sage: lp.get_values([x, y])
|
|
434
|
+
[1.0, 1.0]
|
|
435
|
+
|
|
436
|
+
TESTS::
|
|
437
|
+
|
|
438
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
439
|
+
sage: p = get_solver(solver = "HiGHS")
|
|
440
|
+
sage: p.add_variables(2)
|
|
441
|
+
1
|
|
442
|
+
sage: p.add_linear_constraint([(0, 1), (1, 1)], None, 2.0)
|
|
443
|
+
sage: p.set_objective([1, 1])
|
|
444
|
+
sage: p.solve()
|
|
445
|
+
0
|
|
446
|
+
sage: p.objective_coefficient(0,1)
|
|
447
|
+
sage: p.solve()
|
|
448
|
+
0
|
|
449
|
+
"""
|
|
450
|
+
cdef HighsInt status
|
|
451
|
+
cdef HighsInt model_status
|
|
452
|
+
|
|
453
|
+
# Pure C API call between sig_on/sig_off - no Python objects touched!
|
|
454
|
+
sig_on()
|
|
455
|
+
status = Highs_run(self.highs)
|
|
456
|
+
sig_off()
|
|
457
|
+
|
|
458
|
+
if status != kHighsStatusOk:
|
|
459
|
+
raise MIPSolverException("HiGHS: Solver run failed")
|
|
460
|
+
|
|
461
|
+
# Check model status
|
|
462
|
+
model_status = Highs_getModelStatus(self.highs)
|
|
463
|
+
|
|
464
|
+
if model_status == kHighsModelStatusOptimal:
|
|
465
|
+
return 0 # Success
|
|
466
|
+
elif model_status == kHighsModelStatusModelEmpty:
|
|
467
|
+
return 0 # Empty model is trivially optimal
|
|
468
|
+
elif model_status == kHighsModelStatusInfeasible:
|
|
469
|
+
raise MIPSolverException("HiGHS: Problem is infeasible")
|
|
470
|
+
elif model_status == kHighsModelStatusUnbounded:
|
|
471
|
+
raise MIPSolverException("HiGHS: Problem is unbounded")
|
|
472
|
+
elif model_status == kHighsModelStatusUnboundedOrInfeasible:
|
|
473
|
+
raise MIPSolverException("HiGHS: Problem is unbounded or infeasible")
|
|
474
|
+
elif model_status == kHighsModelStatusTimeLimit:
|
|
475
|
+
raise MIPSolverException("HiGHS: Time limit reached")
|
|
476
|
+
elif model_status == kHighsModelStatusIterationLimit:
|
|
477
|
+
raise MIPSolverException("HiGHS: Iteration limit reached")
|
|
478
|
+
elif model_status == kHighsModelStatusSolutionLimit:
|
|
479
|
+
raise MIPSolverException("HiGHS: Solution limit reached")
|
|
480
|
+
elif model_status == kHighsModelStatusInterrupt:
|
|
481
|
+
raise MIPSolverException("HiGHS: Interrupted by user")
|
|
482
|
+
elif model_status == kHighsModelStatusObjectiveBound:
|
|
483
|
+
return 0 # Objective bound reached - considered success
|
|
484
|
+
elif model_status == kHighsModelStatusObjectiveTarget:
|
|
485
|
+
return 0 # Objective target reached - considered success
|
|
486
|
+
elif model_status == kHighsModelStatusNotset:
|
|
487
|
+
raise MIPSolverException("HiGHS: Model status not set")
|
|
488
|
+
elif model_status == kHighsModelStatusLoadError:
|
|
489
|
+
raise MIPSolverException("HiGHS: Load error")
|
|
490
|
+
elif model_status == kHighsModelStatusModelError:
|
|
491
|
+
raise MIPSolverException("HiGHS: Model error")
|
|
492
|
+
elif model_status == kHighsModelStatusPresolveError:
|
|
493
|
+
raise MIPSolverException("HiGHS: Presolve error")
|
|
494
|
+
elif model_status == kHighsModelStatusSolveError:
|
|
495
|
+
raise MIPSolverException("HiGHS: Solve error")
|
|
496
|
+
elif model_status == kHighsModelStatusPostsolveError:
|
|
497
|
+
raise MIPSolverException("HiGHS: Postsolve error")
|
|
498
|
+
elif model_status == kHighsModelStatusUnknown:
|
|
499
|
+
raise MIPSolverException("HiGHS: Unknown status")
|
|
500
|
+
else:
|
|
501
|
+
raise MIPSolverException(f"HiGHS: Solver failed with unknown model status {model_status}")
|
|
502
|
+
|
|
503
|
+
cpdef get_objective_value(self):
|
|
504
|
+
"""
|
|
505
|
+
Return the value of the objective function.
|
|
506
|
+
|
|
507
|
+
EXAMPLES::
|
|
508
|
+
|
|
509
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
510
|
+
sage: p = get_solver(solver='HiGHS')
|
|
511
|
+
sage: p.add_variables(2)
|
|
512
|
+
1
|
|
513
|
+
sage: p.add_linear_constraint([(0, 1), (1, 1)], None, 2.0)
|
|
514
|
+
sage: p.set_objective([1, 1])
|
|
515
|
+
sage: p.solve()
|
|
516
|
+
0
|
|
517
|
+
sage: p.get_objective_value()
|
|
518
|
+
2.0
|
|
519
|
+
"""
|
|
520
|
+
cdef double obj_value
|
|
521
|
+
|
|
522
|
+
obj_value = Highs_getObjectiveValue(self.highs)
|
|
523
|
+
# HiGHS already includes the offset, so don't add it again
|
|
524
|
+
return obj_value
|
|
525
|
+
|
|
526
|
+
cpdef get_variable_value(self, int variable):
|
|
527
|
+
"""
|
|
528
|
+
Return the value of a variable given by the solver.
|
|
529
|
+
|
|
530
|
+
EXAMPLES::
|
|
531
|
+
|
|
532
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
533
|
+
sage: p = get_solver(solver='HiGHS')
|
|
534
|
+
sage: p.add_variables(2)
|
|
535
|
+
1
|
|
536
|
+
sage: p.add_linear_constraint([(0, 1), (1, 1)], None, 2.0)
|
|
537
|
+
sage: p.set_objective([1, 1])
|
|
538
|
+
sage: p.solve()
|
|
539
|
+
0
|
|
540
|
+
sage: p.get_variable_value(0)
|
|
541
|
+
2.0
|
|
542
|
+
sage: p.get_variable_value(1)
|
|
543
|
+
0.0
|
|
544
|
+
"""
|
|
545
|
+
cdef double* col_value
|
|
546
|
+
cdef HighsInt num_cols
|
|
547
|
+
cdef HighsInt status
|
|
548
|
+
cdef double result
|
|
549
|
+
|
|
550
|
+
num_cols = Highs_getNumCol(self.highs)
|
|
551
|
+
|
|
552
|
+
if variable < 0 or variable >= num_cols:
|
|
553
|
+
raise ValueError(f"Variable index {variable} out of range [0, {num_cols})")
|
|
554
|
+
|
|
555
|
+
# Allocate array for solution
|
|
556
|
+
col_value = <double*> malloc(num_cols * sizeof(double))
|
|
557
|
+
if col_value == NULL:
|
|
558
|
+
raise MemoryError("Failed to allocate memory for solution")
|
|
559
|
+
|
|
560
|
+
try:
|
|
561
|
+
sig_on()
|
|
562
|
+
status = Highs_getSolution(self.highs, col_value, NULL, NULL, NULL)
|
|
563
|
+
sig_off()
|
|
564
|
+
|
|
565
|
+
if status != kHighsStatusOk:
|
|
566
|
+
raise MIPSolverException("HiGHS: Failed to get solution")
|
|
567
|
+
|
|
568
|
+
result = col_value[variable]
|
|
569
|
+
finally:
|
|
570
|
+
free(col_value)
|
|
571
|
+
|
|
572
|
+
return result
|
|
573
|
+
|
|
574
|
+
cpdef int add_variables(self, int number, lower_bound=0.0, upper_bound=None,
|
|
575
|
+
binary=False, continuous=False, integer=False, obj=0.0,
|
|
576
|
+
names=None) except -1:
|
|
577
|
+
"""
|
|
578
|
+
Add ``number`` new variables.
|
|
579
|
+
|
|
580
|
+
This amounts to adding new columns to the matrix. By default,
|
|
581
|
+
the variables are both positive, real and their coefficient in
|
|
582
|
+
the objective function is 0.0.
|
|
583
|
+
|
|
584
|
+
INPUT:
|
|
585
|
+
|
|
586
|
+
- ``n`` -- the number of new variables (must be > 0)
|
|
587
|
+
|
|
588
|
+
- ``lower_bound`` -- the lower bound of the variable (default: 0)
|
|
589
|
+
|
|
590
|
+
- ``upper_bound`` -- the upper bound of the variable (default: ``None``)
|
|
591
|
+
|
|
592
|
+
- ``binary`` -- ``True`` if the variable is binary (default: ``False``)
|
|
593
|
+
|
|
594
|
+
- ``continuous`` -- ``True`` if the variable is binary (default: ``True``)
|
|
595
|
+
|
|
596
|
+
- ``integer`` -- ``True`` if the variable is binary (default: ``False``)
|
|
597
|
+
|
|
598
|
+
- ``obj`` -- coefficient of all variables in the objective function (default: 0.0)
|
|
599
|
+
|
|
600
|
+
- ``names`` -- list of names (default: ``None``)
|
|
601
|
+
|
|
602
|
+
OUTPUT: the index of the variable created last
|
|
603
|
+
|
|
604
|
+
EXAMPLES::
|
|
605
|
+
|
|
606
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
607
|
+
sage: p = get_solver(solver = "HiGHS")
|
|
608
|
+
sage: p.ncols()
|
|
609
|
+
0
|
|
610
|
+
sage: p.add_variables(5)
|
|
611
|
+
4
|
|
612
|
+
sage: p.ncols()
|
|
613
|
+
5
|
|
614
|
+
sage: p.add_variables(2, lower_bound=-2.0, integer=True, obj=42.0, names=['a','b'])
|
|
615
|
+
6
|
|
616
|
+
|
|
617
|
+
TESTS:
|
|
618
|
+
|
|
619
|
+
Check that arguments are used::
|
|
620
|
+
|
|
621
|
+
sage: p.col_bounds(5) # tol 1e-8
|
|
622
|
+
(-2.0, None)
|
|
623
|
+
sage: p.is_variable_integer(5)
|
|
624
|
+
True
|
|
625
|
+
sage: p.col_name(5)
|
|
626
|
+
'a'
|
|
627
|
+
sage: p.objective_coefficient(5)
|
|
628
|
+
42.0
|
|
629
|
+
"""
|
|
630
|
+
cdef int i
|
|
631
|
+
|
|
632
|
+
for i in range(number):
|
|
633
|
+
name = None
|
|
634
|
+
if names is not None and i < len(names):
|
|
635
|
+
name = names[i]
|
|
636
|
+
self.add_variable(lower_bound=lower_bound, upper_bound=upper_bound,
|
|
637
|
+
binary=binary, continuous=continuous, integer=integer,
|
|
638
|
+
obj=obj, name=name)
|
|
639
|
+
|
|
640
|
+
return self.numcols - 1
|
|
641
|
+
|
|
642
|
+
cpdef objective_coefficient(self, int variable, coeff=None):
|
|
643
|
+
"""
|
|
644
|
+
Set or get the coefficient of a variable in the objective function.
|
|
645
|
+
|
|
646
|
+
INPUT:
|
|
647
|
+
|
|
648
|
+
- ``variable`` -- integer; the variable's id
|
|
649
|
+
|
|
650
|
+
- ``coeff`` -- double; its coefficient or ``None`` for
|
|
651
|
+
reading (default: ``None``)
|
|
652
|
+
|
|
653
|
+
EXAMPLES::
|
|
654
|
+
|
|
655
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
656
|
+
sage: p = get_solver(solver = "HiGHS")
|
|
657
|
+
sage: p.add_variable()
|
|
658
|
+
0
|
|
659
|
+
sage: p.objective_coefficient(0)
|
|
660
|
+
0.0
|
|
661
|
+
sage: p.objective_coefficient(0, 2)
|
|
662
|
+
sage: p.objective_coefficient(0)
|
|
663
|
+
2.0
|
|
664
|
+
|
|
665
|
+
TESTS:
|
|
666
|
+
|
|
667
|
+
We sanity check the input that will be passed to HiGHS::
|
|
668
|
+
|
|
669
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
670
|
+
sage: p = get_solver(solver='HiGHS')
|
|
671
|
+
sage: p.objective_coefficient(2)
|
|
672
|
+
Traceback (most recent call last):
|
|
673
|
+
...
|
|
674
|
+
ValueError: invalid variable index 2
|
|
675
|
+
"""
|
|
676
|
+
cdef HighsInt status
|
|
677
|
+
cdef double cost
|
|
678
|
+
cdef HighsInt num_col
|
|
679
|
+
cdef HighsInt num_nz
|
|
680
|
+
|
|
681
|
+
if variable < 0 or variable >= self.numcols:
|
|
682
|
+
raise ValueError(f"invalid variable index {variable}")
|
|
683
|
+
|
|
684
|
+
if coeff is None:
|
|
685
|
+
# Get coefficient using Highs_getColsByRange
|
|
686
|
+
sig_on()
|
|
687
|
+
status = Highs_getColsByRange(self.highs, variable, variable,
|
|
688
|
+
&num_col, &cost, NULL, NULL,
|
|
689
|
+
&num_nz, NULL, NULL, NULL)
|
|
690
|
+
sig_off()
|
|
691
|
+
|
|
692
|
+
if status != kHighsStatusOk:
|
|
693
|
+
raise MIPSolverException("HiGHS: Failed to get objective coefficient")
|
|
694
|
+
return cost
|
|
695
|
+
else:
|
|
696
|
+
# Set coefficient
|
|
697
|
+
sig_on()
|
|
698
|
+
status = Highs_changeColCost(self.highs, variable, float(coeff))
|
|
699
|
+
sig_off()
|
|
700
|
+
|
|
701
|
+
if status != kHighsStatusOk:
|
|
702
|
+
raise MIPSolverException("HiGHS: Failed to set objective coefficient")
|
|
703
|
+
|
|
704
|
+
cpdef problem_name(self, name=None):
|
|
705
|
+
"""
|
|
706
|
+
Return or define the problem's name.
|
|
707
|
+
|
|
708
|
+
INPUT:
|
|
709
|
+
|
|
710
|
+
- ``name`` -- string; the problem's name. When set to
|
|
711
|
+
``None`` (default), the method returns the problem's name.
|
|
712
|
+
|
|
713
|
+
EXAMPLES::
|
|
714
|
+
|
|
715
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
716
|
+
sage: p = get_solver(solver = "HiGHS")
|
|
717
|
+
sage: p.problem_name("There once was a french fry")
|
|
718
|
+
sage: print(p.problem_name())
|
|
719
|
+
There once was a french fry
|
|
720
|
+
"""
|
|
721
|
+
if name is None:
|
|
722
|
+
return self.prob_name
|
|
723
|
+
else:
|
|
724
|
+
self.prob_name = str(name)
|
|
725
|
+
|
|
726
|
+
cpdef set_objective(self, list coeff, d=0.0):
|
|
727
|
+
"""
|
|
728
|
+
Set the objective function.
|
|
729
|
+
|
|
730
|
+
INPUT:
|
|
731
|
+
|
|
732
|
+
- ``coeff`` -- list of real values, whose i-th element is the
|
|
733
|
+
coefficient of the i-th variable in the objective function
|
|
734
|
+
- ``d`` -- constant term in objective function (default: 0.0)
|
|
735
|
+
|
|
736
|
+
EXAMPLES::
|
|
737
|
+
|
|
738
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
739
|
+
sage: p = get_solver(solver='HiGHS')
|
|
740
|
+
sage: p.add_variables(5)
|
|
741
|
+
4
|
|
742
|
+
sage: p.set_objective([1, 1, 2, 1, 3])
|
|
743
|
+
"""
|
|
744
|
+
cdef int i
|
|
745
|
+
for i in range(len(coeff)):
|
|
746
|
+
if i < self.numcols:
|
|
747
|
+
self.objective_coefficient(i, coeff[i])
|
|
748
|
+
|
|
749
|
+
self.obj_constant_term = d
|
|
750
|
+
|
|
751
|
+
# Set offset in HiGHS
|
|
752
|
+
sig_on()
|
|
753
|
+
Highs_changeObjectiveOffset(self.highs, d)
|
|
754
|
+
sig_off()
|
|
755
|
+
|
|
756
|
+
cpdef set_verbosity(self, int level):
|
|
757
|
+
"""
|
|
758
|
+
Set the log (verbosity) level.
|
|
759
|
+
|
|
760
|
+
INPUT:
|
|
761
|
+
|
|
762
|
+
- ``level`` -- integer; from 0 (no verbosity) to 1
|
|
763
|
+
|
|
764
|
+
EXAMPLES::
|
|
765
|
+
|
|
766
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
767
|
+
sage: p = get_solver(solver='HiGHS')
|
|
768
|
+
sage: p.set_verbosity(0)
|
|
769
|
+
"""
|
|
770
|
+
cdef HighsInt status
|
|
771
|
+
cdef bint enable_output
|
|
772
|
+
|
|
773
|
+
if level == 0:
|
|
774
|
+
enable_output = False
|
|
775
|
+
elif level == 1:
|
|
776
|
+
enable_output = True
|
|
777
|
+
else:
|
|
778
|
+
raise ValueError("Invalid verbosity level. Must be 0 or 1.")
|
|
779
|
+
|
|
780
|
+
# output_flag is the master switch for all HiGHS output
|
|
781
|
+
status = Highs_setBoolOptionValue(self.highs, b"output_flag", enable_output)
|
|
782
|
+
if status != kHighsStatusOk:
|
|
783
|
+
raise MIPSolverException("HiGHS: Failed to set output_flag")
|
|
784
|
+
|
|
785
|
+
# log_to_console controls whether log messages go to console
|
|
786
|
+
status = Highs_setBoolOptionValue(self.highs, b"log_to_console", enable_output)
|
|
787
|
+
if status != kHighsStatusOk:
|
|
788
|
+
raise MIPSolverException("HiGHS: Failed to set log_to_console")
|
|
789
|
+
|
|
790
|
+
cpdef add_linear_constraint(self, coefficients, lower_bound, upper_bound, name=None):
|
|
791
|
+
"""
|
|
792
|
+
Add a linear constraint.
|
|
793
|
+
|
|
794
|
+
INPUT:
|
|
795
|
+
|
|
796
|
+
- ``coefficients`` -- an iterable of pairs ``(i, v)`` where ``i`` is a
|
|
797
|
+
variable index and ``v`` is a value
|
|
798
|
+
- ``lower_bound`` -- a lower bound, either a real value or ``None``
|
|
799
|
+
- ``upper_bound`` -- an upper bound, either a real value or ``None``
|
|
800
|
+
- ``name`` -- optional name for this constraint
|
|
801
|
+
|
|
802
|
+
EXAMPLES::
|
|
803
|
+
|
|
804
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
805
|
+
sage: p = get_solver(solver='HiGHS')
|
|
806
|
+
sage: p.add_variables(5)
|
|
807
|
+
4
|
|
808
|
+
sage: p.add_linear_constraint([(0, 1), (1, 1)], None, 2.0)
|
|
809
|
+
"""
|
|
810
|
+
cdef double lb, ub
|
|
811
|
+
cdef HighsInt num_nz
|
|
812
|
+
cdef HighsInt* indices
|
|
813
|
+
cdef double* values
|
|
814
|
+
cdef HighsInt status
|
|
815
|
+
cdef int i
|
|
816
|
+
|
|
817
|
+
# Convert bounds
|
|
818
|
+
if lower_bound is None:
|
|
819
|
+
lb = -Highs_getInfinity(self.highs)
|
|
820
|
+
else:
|
|
821
|
+
lb = float(lower_bound)
|
|
822
|
+
|
|
823
|
+
if upper_bound is None:
|
|
824
|
+
ub = Highs_getInfinity(self.highs)
|
|
825
|
+
else:
|
|
826
|
+
ub = float(upper_bound)
|
|
827
|
+
|
|
828
|
+
# Build coefficient arrays
|
|
829
|
+
coeff_list = list(coefficients)
|
|
830
|
+
num_nz = len(coeff_list)
|
|
831
|
+
|
|
832
|
+
if num_nz == 0:
|
|
833
|
+
# Empty constraint
|
|
834
|
+
sig_on()
|
|
835
|
+
status = Highs_addRow(self.highs, lb, ub, 0, NULL, NULL)
|
|
836
|
+
sig_off()
|
|
837
|
+
else:
|
|
838
|
+
# Allocate arrays
|
|
839
|
+
indices = <HighsInt*> malloc(num_nz * sizeof(HighsInt))
|
|
840
|
+
values = <double*> malloc(num_nz * sizeof(double))
|
|
841
|
+
|
|
842
|
+
if indices == NULL or values == NULL:
|
|
843
|
+
free(indices)
|
|
844
|
+
free(values)
|
|
845
|
+
raise MemoryError("Failed to allocate memory for constraint")
|
|
846
|
+
|
|
847
|
+
try:
|
|
848
|
+
# Fill arrays
|
|
849
|
+
for i in range(num_nz):
|
|
850
|
+
var_idx, coeff_val = coeff_list[i]
|
|
851
|
+
if var_idx < 0 or var_idx >= self.ncols():
|
|
852
|
+
raise ValueError(f"invalid variable index {var_idx}")
|
|
853
|
+
indices[i] = var_idx
|
|
854
|
+
values[i] = float(coeff_val)
|
|
855
|
+
|
|
856
|
+
# Add constraint
|
|
857
|
+
sig_on()
|
|
858
|
+
status = Highs_addRow(self.highs, lb, ub, num_nz, indices, values)
|
|
859
|
+
sig_off()
|
|
860
|
+
finally:
|
|
861
|
+
free(indices)
|
|
862
|
+
free(values)
|
|
863
|
+
|
|
864
|
+
if status != kHighsStatusOk:
|
|
865
|
+
raise MIPSolverException("HiGHS: Failed to add constraint")
|
|
866
|
+
|
|
867
|
+
# Handle name
|
|
868
|
+
if name is not None:
|
|
869
|
+
self.row_name_var[name] = self.numrows
|
|
870
|
+
|
|
871
|
+
self.numrows += 1
|
|
872
|
+
|
|
873
|
+
cpdef add_linear_constraints(self, int number, lower_bound, upper_bound, names=None):
|
|
874
|
+
"""
|
|
875
|
+
Add ``number`` linear constraints.
|
|
876
|
+
|
|
877
|
+
INPUT:
|
|
878
|
+
|
|
879
|
+
- ``number`` -- integer; the number of constraints to add
|
|
880
|
+
|
|
881
|
+
- ``lower_bound`` -- a lower bound, either a real value or ``None``
|
|
882
|
+
|
|
883
|
+
- ``upper_bound`` -- an upper bound, either a real value or ``None``
|
|
884
|
+
|
|
885
|
+
- ``names`` -- an optional list of names (default: ``None``)
|
|
886
|
+
|
|
887
|
+
EXAMPLES::
|
|
888
|
+
|
|
889
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
890
|
+
sage: p = get_solver(solver='HiGHS')
|
|
891
|
+
sage: p.add_variables(5)
|
|
892
|
+
4
|
|
893
|
+
sage: p.add_linear_constraints(5, None, 2)
|
|
894
|
+
sage: p.row_bounds(4)
|
|
895
|
+
(None, 2.0)
|
|
896
|
+
sage: p.add_linear_constraints(2, None, 2, names=['foo','bar'])
|
|
897
|
+
"""
|
|
898
|
+
cdef int i
|
|
899
|
+
cdef double lb, ub
|
|
900
|
+
cdef HighsInt status
|
|
901
|
+
|
|
902
|
+
# Convert bounds
|
|
903
|
+
if lower_bound is None:
|
|
904
|
+
lb = -Highs_getInfinity(self.highs)
|
|
905
|
+
else:
|
|
906
|
+
lb = float(lower_bound)
|
|
907
|
+
|
|
908
|
+
if upper_bound is None:
|
|
909
|
+
ub = Highs_getInfinity(self.highs)
|
|
910
|
+
else:
|
|
911
|
+
ub = float(upper_bound)
|
|
912
|
+
|
|
913
|
+
# Add empty constraints
|
|
914
|
+
for i in range(number):
|
|
915
|
+
sig_on()
|
|
916
|
+
status = Highs_addRow(self.highs, lb, ub, 0, NULL, NULL)
|
|
917
|
+
sig_off()
|
|
918
|
+
|
|
919
|
+
if status != kHighsStatusOk:
|
|
920
|
+
raise MIPSolverException("HiGHS: Failed to add constraint")
|
|
921
|
+
|
|
922
|
+
if names is not None and i < len(names):
|
|
923
|
+
name = names[i]
|
|
924
|
+
if name is not None:
|
|
925
|
+
self.row_name_var[name] = self.numrows
|
|
926
|
+
|
|
927
|
+
self.numrows += 1
|
|
928
|
+
|
|
929
|
+
cpdef int ncols(self) noexcept:
|
|
930
|
+
"""
|
|
931
|
+
Return the number of columns/variables.
|
|
932
|
+
|
|
933
|
+
EXAMPLES::
|
|
934
|
+
|
|
935
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
936
|
+
sage: p = get_solver(solver='HiGHS')
|
|
937
|
+
sage: p.ncols()
|
|
938
|
+
0
|
|
939
|
+
sage: p.add_variables(2)
|
|
940
|
+
1
|
|
941
|
+
sage: p.ncols()
|
|
942
|
+
2
|
|
943
|
+
"""
|
|
944
|
+
return self.numcols
|
|
945
|
+
|
|
946
|
+
cpdef int nrows(self) noexcept:
|
|
947
|
+
"""
|
|
948
|
+
Return the number of rows/constraints.
|
|
949
|
+
|
|
950
|
+
EXAMPLES::
|
|
951
|
+
|
|
952
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
953
|
+
sage: p = get_solver(solver='HiGHS')
|
|
954
|
+
sage: p.nrows()
|
|
955
|
+
0
|
|
956
|
+
sage: p.add_variables(2)
|
|
957
|
+
1
|
|
958
|
+
sage: p.add_linear_constraint([(0, 1), (1, 1)], None, 2.0)
|
|
959
|
+
sage: p.nrows()
|
|
960
|
+
1
|
|
961
|
+
"""
|
|
962
|
+
return self.numrows
|
|
963
|
+
|
|
964
|
+
cpdef bint is_maximization(self) noexcept:
|
|
965
|
+
"""
|
|
966
|
+
Test whether the problem is a maximization.
|
|
967
|
+
|
|
968
|
+
EXAMPLES::
|
|
969
|
+
|
|
970
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
971
|
+
sage: p = get_solver(solver='HiGHS')
|
|
972
|
+
sage: p.is_maximization()
|
|
973
|
+
True
|
|
974
|
+
"""
|
|
975
|
+
cdef HighsInt sense, status
|
|
976
|
+
status = Highs_getObjectiveSense(self.highs, &sense)
|
|
977
|
+
if status != kHighsStatusOk:
|
|
978
|
+
return True # default to maximize
|
|
979
|
+
return sense == kHighsObjSenseMaximize
|
|
980
|
+
|
|
981
|
+
cpdef get_row_prim(self, int i):
|
|
982
|
+
"""
|
|
983
|
+
Return the value of the auxiliary variable associated with i-th row.
|
|
984
|
+
|
|
985
|
+
.. NOTE::
|
|
986
|
+
|
|
987
|
+
Behaviour is undefined unless ``solve`` has been called before.
|
|
988
|
+
|
|
989
|
+
EXAMPLES::
|
|
990
|
+
|
|
991
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
992
|
+
sage: lp = get_solver(solver='HiGHS')
|
|
993
|
+
sage: lp.add_variables(3)
|
|
994
|
+
2
|
|
995
|
+
sage: lp.add_linear_constraint(list(zip([0, 1, 2], [8, 6, 1])), None, 48)
|
|
996
|
+
sage: lp.add_linear_constraint(list(zip([0, 1, 2], [4, 2, 1.5])), None, 20)
|
|
997
|
+
sage: lp.add_linear_constraint(list(zip([0, 1, 2], [2, 1.5, 0.5])), None, 8)
|
|
998
|
+
sage: lp.set_objective([60, 30, 20])
|
|
999
|
+
sage: lp.solve()
|
|
1000
|
+
0
|
|
1001
|
+
sage: lp.get_objective_value()
|
|
1002
|
+
280.0
|
|
1003
|
+
sage: lp.get_row_prim(0)
|
|
1004
|
+
24.0
|
|
1005
|
+
sage: lp.get_row_prim(1)
|
|
1006
|
+
20.0
|
|
1007
|
+
sage: lp.get_row_prim(2)
|
|
1008
|
+
8.0
|
|
1009
|
+
|
|
1010
|
+
TESTS:
|
|
1011
|
+
|
|
1012
|
+
We sanity check the input::
|
|
1013
|
+
|
|
1014
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
1015
|
+
sage: p = get_solver(solver='HiGHS')
|
|
1016
|
+
sage: p.get_row_prim(2)
|
|
1017
|
+
Traceback (most recent call last):
|
|
1018
|
+
...
|
|
1019
|
+
ValueError: Row index 2 out of range ...
|
|
1020
|
+
"""
|
|
1021
|
+
cdef double* row_value
|
|
1022
|
+
cdef HighsInt num_rows
|
|
1023
|
+
cdef HighsInt status
|
|
1024
|
+
cdef double result
|
|
1025
|
+
|
|
1026
|
+
num_rows = Highs_getNumRow(self.highs)
|
|
1027
|
+
|
|
1028
|
+
if i < 0 or i >= num_rows:
|
|
1029
|
+
raise ValueError(f"Row index {i} out of range [0, {num_rows})")
|
|
1030
|
+
|
|
1031
|
+
row_value = <double*> malloc(num_rows * sizeof(double))
|
|
1032
|
+
if row_value == NULL:
|
|
1033
|
+
raise MemoryError("Failed to allocate memory")
|
|
1034
|
+
|
|
1035
|
+
try:
|
|
1036
|
+
sig_on()
|
|
1037
|
+
status = Highs_getSolution(self.highs, NULL, NULL, row_value, NULL)
|
|
1038
|
+
sig_off()
|
|
1039
|
+
|
|
1040
|
+
if status != kHighsStatusOk:
|
|
1041
|
+
raise MIPSolverException("HiGHS: Failed to get solution")
|
|
1042
|
+
|
|
1043
|
+
result = row_value[i]
|
|
1044
|
+
finally:
|
|
1045
|
+
free(row_value)
|
|
1046
|
+
|
|
1047
|
+
return result
|
|
1048
|
+
|
|
1049
|
+
cpdef double get_row_dual(self, int i) except? -1:
|
|
1050
|
+
"""
|
|
1051
|
+
Return the dual value of a constraint.
|
|
1052
|
+
|
|
1053
|
+
The dual value of the i-th row is also the value of the i-th variable
|
|
1054
|
+
of the dual problem.
|
|
1055
|
+
|
|
1056
|
+
The dual value of a constraint is the shadow price of the constraint.
|
|
1057
|
+
The shadow price is the amount by which the objective value will change
|
|
1058
|
+
if the constraint's bounds change by one unit under the precondition
|
|
1059
|
+
that the basis remains the same.
|
|
1060
|
+
|
|
1061
|
+
INPUT:
|
|
1062
|
+
|
|
1063
|
+
- ``i`` -- the index of the constraint
|
|
1064
|
+
|
|
1065
|
+
.. NOTE::
|
|
1066
|
+
|
|
1067
|
+
Behaviour is undefined unless ``solve`` has been called before.
|
|
1068
|
+
|
|
1069
|
+
EXAMPLES::
|
|
1070
|
+
|
|
1071
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
1072
|
+
sage: lp = get_solver(solver='HiGHS')
|
|
1073
|
+
sage: lp.add_variables(3)
|
|
1074
|
+
2
|
|
1075
|
+
sage: lp.add_linear_constraint(list(zip([0, 1, 2], [8, 6, 1])), None, 48)
|
|
1076
|
+
sage: lp.add_linear_constraint(list(zip([0, 1, 2], [4, 2, 1.5])), None, 20)
|
|
1077
|
+
sage: lp.add_linear_constraint(list(zip([0, 1, 2], [2, 1.5, 0.5])), None, 8)
|
|
1078
|
+
sage: lp.set_objective([60, 30, 20])
|
|
1079
|
+
sage: lp.solve()
|
|
1080
|
+
0
|
|
1081
|
+
sage: lp.get_row_dual(0) # tol 1e-6
|
|
1082
|
+
0.0
|
|
1083
|
+
sage: lp.get_row_dual(1) # tol 1e-6
|
|
1084
|
+
10.0
|
|
1085
|
+
sage: lp.get_row_dual(2) # tol 1e-6
|
|
1086
|
+
10.0
|
|
1087
|
+
|
|
1088
|
+
TESTS:
|
|
1089
|
+
|
|
1090
|
+
We sanity check the input::
|
|
1091
|
+
|
|
1092
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
1093
|
+
sage: p = get_solver(solver='HiGHS')
|
|
1094
|
+
sage: p.get_row_dual(2)
|
|
1095
|
+
Traceback (most recent call last):
|
|
1096
|
+
...
|
|
1097
|
+
ValueError: Row index 2 out of range ...
|
|
1098
|
+
"""
|
|
1099
|
+
cdef double* row_dual
|
|
1100
|
+
cdef HighsInt num_rows
|
|
1101
|
+
cdef HighsInt status
|
|
1102
|
+
cdef double result
|
|
1103
|
+
|
|
1104
|
+
num_rows = Highs_getNumRow(self.highs)
|
|
1105
|
+
|
|
1106
|
+
if i < 0 or i >= num_rows:
|
|
1107
|
+
raise ValueError(f"Row index {i} out of range [0, {num_rows})")
|
|
1108
|
+
|
|
1109
|
+
row_dual = <double*> malloc(num_rows * sizeof(double))
|
|
1110
|
+
if row_dual == NULL:
|
|
1111
|
+
raise MemoryError("Failed to allocate memory")
|
|
1112
|
+
|
|
1113
|
+
try:
|
|
1114
|
+
sig_on()
|
|
1115
|
+
status = Highs_getSolution(self.highs, NULL, NULL, NULL, row_dual)
|
|
1116
|
+
sig_off()
|
|
1117
|
+
|
|
1118
|
+
if status != kHighsStatusOk:
|
|
1119
|
+
raise MIPSolverException("HiGHS: Failed to get dual solution")
|
|
1120
|
+
|
|
1121
|
+
result = row_dual[i]
|
|
1122
|
+
finally:
|
|
1123
|
+
free(row_dual)
|
|
1124
|
+
|
|
1125
|
+
return result
|
|
1126
|
+
|
|
1127
|
+
cpdef double get_col_dual(self, int j) except? -1:
|
|
1128
|
+
"""
|
|
1129
|
+
Return the dual value (reduced cost) of a variable.
|
|
1130
|
+
|
|
1131
|
+
The dual value is the reduced cost of a variable.
|
|
1132
|
+
The reduced cost is the amount by which the objective coefficient
|
|
1133
|
+
of a non-basic variable has to change to become a basic variable.
|
|
1134
|
+
|
|
1135
|
+
INPUT:
|
|
1136
|
+
|
|
1137
|
+
- ``j`` -- the index of the variable
|
|
1138
|
+
|
|
1139
|
+
.. NOTE::
|
|
1140
|
+
|
|
1141
|
+
Behaviour is undefined unless ``solve`` has been called before.
|
|
1142
|
+
|
|
1143
|
+
EXAMPLES::
|
|
1144
|
+
|
|
1145
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
1146
|
+
sage: p = get_solver(solver='HiGHS')
|
|
1147
|
+
sage: p.add_variables(3)
|
|
1148
|
+
2
|
|
1149
|
+
sage: p.add_linear_constraint(list(zip([0, 1, 2], [8, 6, 1])), None, 48)
|
|
1150
|
+
sage: p.add_linear_constraint(list(zip([0, 1, 2], [4, 2, 1.5])), None, 20)
|
|
1151
|
+
sage: p.add_linear_constraint(list(zip([0, 1, 2], [2, 1.5, 0.5])), None, 8)
|
|
1152
|
+
sage: p.set_objective([60, 30, 20])
|
|
1153
|
+
sage: p.solve()
|
|
1154
|
+
0
|
|
1155
|
+
sage: p.get_col_dual(0) # tol 1e-6
|
|
1156
|
+
0.0
|
|
1157
|
+
sage: p.get_col_dual(1) # tol 1e-6
|
|
1158
|
+
-5.0
|
|
1159
|
+
sage: p.get_col_dual(2) # tol 1e-6
|
|
1160
|
+
0.0
|
|
1161
|
+
|
|
1162
|
+
TESTS:
|
|
1163
|
+
|
|
1164
|
+
We sanity check the input::
|
|
1165
|
+
|
|
1166
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
1167
|
+
sage: p = get_solver(solver='HiGHS')
|
|
1168
|
+
sage: p.get_col_dual(2)
|
|
1169
|
+
Traceback (most recent call last):
|
|
1170
|
+
...
|
|
1171
|
+
ValueError: Variable index 2 out of range ...
|
|
1172
|
+
"""
|
|
1173
|
+
cdef double* col_dual
|
|
1174
|
+
cdef HighsInt num_cols
|
|
1175
|
+
cdef HighsInt status
|
|
1176
|
+
cdef double result
|
|
1177
|
+
|
|
1178
|
+
num_cols = Highs_getNumCol(self.highs)
|
|
1179
|
+
|
|
1180
|
+
if j < 0 or j >= num_cols:
|
|
1181
|
+
raise ValueError(f"Variable index {j} out of range [0, {num_cols})")
|
|
1182
|
+
|
|
1183
|
+
col_dual = <double*> malloc(num_cols * sizeof(double))
|
|
1184
|
+
if col_dual == NULL:
|
|
1185
|
+
raise MemoryError("Failed to allocate memory")
|
|
1186
|
+
|
|
1187
|
+
try:
|
|
1188
|
+
sig_on()
|
|
1189
|
+
status = Highs_getSolution(self.highs, NULL, col_dual, NULL, NULL)
|
|
1190
|
+
sig_off()
|
|
1191
|
+
|
|
1192
|
+
if status != kHighsStatusOk:
|
|
1193
|
+
raise MIPSolverException("HiGHS: Failed to get dual solution")
|
|
1194
|
+
|
|
1195
|
+
result = col_dual[j]
|
|
1196
|
+
finally:
|
|
1197
|
+
free(col_dual)
|
|
1198
|
+
|
|
1199
|
+
return result
|
|
1200
|
+
|
|
1201
|
+
cpdef best_known_objective_bound(self):
|
|
1202
|
+
"""
|
|
1203
|
+
Return the value of the currently best known bound.
|
|
1204
|
+
|
|
1205
|
+
This method returns the current best upper (resp. lower) bound on the
|
|
1206
|
+
optimal value of the objective function in a maximization
|
|
1207
|
+
(resp. minimization) problem. It is equal to the output of
|
|
1208
|
+
:meth:`get_objective_value` if the MILP found an optimal solution, but
|
|
1209
|
+
it can differ if it was interrupted manually or after a time limit (cf
|
|
1210
|
+
:meth:`solver_parameter`).
|
|
1211
|
+
|
|
1212
|
+
.. NOTE::
|
|
1213
|
+
|
|
1214
|
+
Has no meaning unless ``solve`` has been called before.
|
|
1215
|
+
|
|
1216
|
+
EXAMPLES::
|
|
1217
|
+
|
|
1218
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
1219
|
+
sage: p = get_solver(solver='HiGHS')
|
|
1220
|
+
sage: p.add_variables(2)
|
|
1221
|
+
1
|
|
1222
|
+
sage: p.add_linear_constraint([(0, 1), (1, 1)], None, 2.0)
|
|
1223
|
+
sage: p.set_objective([1, 1])
|
|
1224
|
+
sage: p.solve()
|
|
1225
|
+
0
|
|
1226
|
+
sage: p.best_known_objective_bound()
|
|
1227
|
+
2.0
|
|
1228
|
+
|
|
1229
|
+
TESTS::
|
|
1230
|
+
sage: # needs sage.graphs
|
|
1231
|
+
sage: g = graphs.CubeGraph(9)
|
|
1232
|
+
sage: p = MixedIntegerLinearProgram(solver='HiGHS')
|
|
1233
|
+
sage: p.solver_parameter("mip_rel_gap",100)
|
|
1234
|
+
sage: b = p.new_variable(binary=True)
|
|
1235
|
+
sage: p.set_objective(p.sum(b[v] for v in g))
|
|
1236
|
+
sage: for v in g:
|
|
1237
|
+
....: p.add_constraint(b[v]+p.sum(b[u] for u in g.neighbors(v)) <= 1)
|
|
1238
|
+
sage: p.add_constraint(b[v] == 1) # Force an easy non-0 solution
|
|
1239
|
+
sage: p.solve() # rel tol 100
|
|
1240
|
+
2.0
|
|
1241
|
+
sage: backend = p.get_backend()
|
|
1242
|
+
sage: backend.best_known_objective_bound() # abs tol 1e-6
|
|
1243
|
+
48.0
|
|
1244
|
+
"""
|
|
1245
|
+
cdef double mip_dual_bound
|
|
1246
|
+
cdef HighsInt status
|
|
1247
|
+
cdef HighsInt i, var_type
|
|
1248
|
+
cdef bint is_mip = False
|
|
1249
|
+
|
|
1250
|
+
# Check if this is a MIP problem (has integer variables)
|
|
1251
|
+
for i in range(self.numcols):
|
|
1252
|
+
status = Highs_getColIntegrality(self.highs, i, &var_type)
|
|
1253
|
+
if status != kHighsStatusOk:
|
|
1254
|
+
continue
|
|
1255
|
+
if var_type == kHighsVarTypeInteger:
|
|
1256
|
+
is_mip = True
|
|
1257
|
+
break
|
|
1258
|
+
|
|
1259
|
+
if not is_mip:
|
|
1260
|
+
# For LP problems, the bound equals the objective value
|
|
1261
|
+
return self.get_objective_value()
|
|
1262
|
+
|
|
1263
|
+
# Get the MIP dual bound using info query
|
|
1264
|
+
status = Highs_getDoubleInfoValue(self.highs, b"mip_dual_bound", &mip_dual_bound)
|
|
1265
|
+
if status != kHighsStatusOk:
|
|
1266
|
+
# If not available, return the objective value
|
|
1267
|
+
return ValueError("MIP dual bound not available")
|
|
1268
|
+
# HiGHS already includes the offset in the dual bound
|
|
1269
|
+
return mip_dual_bound
|
|
1270
|
+
|
|
1271
|
+
cpdef get_relative_objective_gap(self):
|
|
1272
|
+
"""
|
|
1273
|
+
Return the relative objective gap of the best known solution.
|
|
1274
|
+
|
|
1275
|
+
For a minimization problem, this value is computed by
|
|
1276
|
+
`(\texttt{bestinteger} - \texttt{bestobjective}) / (1e-10 +
|
|
1277
|
+
|\texttt{bestobjective}|)`, where ``bestinteger`` is the value returned
|
|
1278
|
+
by :meth:`get_objective_value` and ``bestobjective`` is the value
|
|
1279
|
+
returned by :meth:`best_known_objective_bound`. For a maximization
|
|
1280
|
+
problem, the value is computed by `(\texttt{bestobjective} -
|
|
1281
|
+
\texttt{bestinteger}) / (1e-10 + |\texttt:bestobjective}|)`.
|
|
1282
|
+
|
|
1283
|
+
.. NOTE::
|
|
1284
|
+
|
|
1285
|
+
Has no meaning unless ``solve`` has been called before.
|
|
1286
|
+
|
|
1287
|
+
EXAMPLES::
|
|
1288
|
+
|
|
1289
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
1290
|
+
sage: p = get_solver(solver='HiGHS')
|
|
1291
|
+
sage: p.add_variables(2)
|
|
1292
|
+
1
|
|
1293
|
+
sage: p.add_linear_constraint([(0, 1), (1, 1)], None, 2.0)
|
|
1294
|
+
sage: p.set_objective([1, 1])
|
|
1295
|
+
sage: p.solve()
|
|
1296
|
+
0
|
|
1297
|
+
sage: p.get_relative_objective_gap()
|
|
1298
|
+
0.0
|
|
1299
|
+
"""
|
|
1300
|
+
cdef double gap
|
|
1301
|
+
cdef HighsInt status
|
|
1302
|
+
cdef HighsInt i, var_type
|
|
1303
|
+
cdef bint is_mip = False
|
|
1304
|
+
|
|
1305
|
+
# Check if this is a MIP problem (has integer variables)
|
|
1306
|
+
for i in range(self.numcols):
|
|
1307
|
+
status = Highs_getColIntegrality(self.highs, i, &var_type)
|
|
1308
|
+
if status != kHighsStatusOk:
|
|
1309
|
+
continue
|
|
1310
|
+
if var_type == kHighsVarTypeInteger:
|
|
1311
|
+
is_mip = True
|
|
1312
|
+
break
|
|
1313
|
+
|
|
1314
|
+
if not is_mip:
|
|
1315
|
+
# For LP problems, the gap is 0
|
|
1316
|
+
return 0.0
|
|
1317
|
+
|
|
1318
|
+
# Get the MIP gap using info query
|
|
1319
|
+
status = Highs_getDoubleInfoValue(self.highs, b"mip_gap", &gap)
|
|
1320
|
+
if status != kHighsStatusOk:
|
|
1321
|
+
# If not available, return 0
|
|
1322
|
+
return ValueError("MIP gap not available")
|
|
1323
|
+
return gap
|
|
1324
|
+
|
|
1325
|
+
cpdef variable_upper_bound(self, int index, value=None):
|
|
1326
|
+
"""
|
|
1327
|
+
Set or get the upper bound of a variable.
|
|
1328
|
+
|
|
1329
|
+
INPUT:
|
|
1330
|
+
|
|
1331
|
+
- ``index`` -- the variable's id
|
|
1332
|
+
- ``value`` -- real value or ``None`` to get the current value
|
|
1333
|
+
|
|
1334
|
+
EXAMPLES::
|
|
1335
|
+
|
|
1336
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
1337
|
+
sage: p = get_solver(solver='HiGHS')
|
|
1338
|
+
sage: p.add_variable()
|
|
1339
|
+
0
|
|
1340
|
+
sage: p.variable_upper_bound(0)
|
|
1341
|
+
sage: p.variable_upper_bound(0, 10.0)
|
|
1342
|
+
sage: p.variable_upper_bound(0)
|
|
1343
|
+
10.0
|
|
1344
|
+
|
|
1345
|
+
TESTS:
|
|
1346
|
+
|
|
1347
|
+
Check that invalid indices raise errors::
|
|
1348
|
+
|
|
1349
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
1350
|
+
sage: p = get_solver(solver='HiGHS')
|
|
1351
|
+
sage: p.variable_upper_bound(2)
|
|
1352
|
+
Traceback (most recent call last):
|
|
1353
|
+
...
|
|
1354
|
+
ValueError: invalid variable index 2
|
|
1355
|
+
sage: p.variable_upper_bound(-1)
|
|
1356
|
+
Traceback (most recent call last):
|
|
1357
|
+
...
|
|
1358
|
+
ValueError: invalid variable index -1
|
|
1359
|
+
sage: p.add_variable()
|
|
1360
|
+
0
|
|
1361
|
+
sage: p.variable_upper_bound(3, 5)
|
|
1362
|
+
Traceback (most recent call last):
|
|
1363
|
+
...
|
|
1364
|
+
ValueError: invalid variable index 3
|
|
1365
|
+
"""
|
|
1366
|
+
cdef double lb, ub
|
|
1367
|
+
cdef HighsInt status
|
|
1368
|
+
|
|
1369
|
+
if index < 0 or index >= self.numcols:
|
|
1370
|
+
raise ValueError(f"invalid variable index {index}")
|
|
1371
|
+
|
|
1372
|
+
if value is None:
|
|
1373
|
+
# Get current bound
|
|
1374
|
+
self._get_col_bounds(index, &lb, &ub)
|
|
1375
|
+
|
|
1376
|
+
if ub >= Highs_getInfinity(self.highs) - 1:
|
|
1377
|
+
return None
|
|
1378
|
+
else:
|
|
1379
|
+
return ub
|
|
1380
|
+
else:
|
|
1381
|
+
# Set new bound
|
|
1382
|
+
self._get_col_bounds(index, &lb, &ub)
|
|
1383
|
+
|
|
1384
|
+
if value is None:
|
|
1385
|
+
ub = Highs_getInfinity(self.highs)
|
|
1386
|
+
else:
|
|
1387
|
+
ub = float(value)
|
|
1388
|
+
|
|
1389
|
+
sig_on()
|
|
1390
|
+
status = Highs_changeColBounds(self.highs, index, lb, ub)
|
|
1391
|
+
sig_off()
|
|
1392
|
+
|
|
1393
|
+
if status != kHighsStatusOk:
|
|
1394
|
+
raise MIPSolverException("HiGHS: Failed to set variable upper bound")
|
|
1395
|
+
|
|
1396
|
+
cpdef variable_lower_bound(self, int index, value=None):
|
|
1397
|
+
"""
|
|
1398
|
+
Set or get the lower bound of a variable.
|
|
1399
|
+
|
|
1400
|
+
INPUT:
|
|
1401
|
+
|
|
1402
|
+
- ``index`` -- the variable's id
|
|
1403
|
+
- ``value`` -- real value or ``None`` to get the current value
|
|
1404
|
+
|
|
1405
|
+
EXAMPLES::
|
|
1406
|
+
|
|
1407
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
1408
|
+
sage: p = get_solver(solver='HiGHS')
|
|
1409
|
+
sage: p.add_variable()
|
|
1410
|
+
0
|
|
1411
|
+
sage: p.variable_lower_bound(0)
|
|
1412
|
+
0.0
|
|
1413
|
+
sage: p.variable_lower_bound(0, -10.0)
|
|
1414
|
+
sage: p.variable_lower_bound(0)
|
|
1415
|
+
-10.0
|
|
1416
|
+
|
|
1417
|
+
TESTS:
|
|
1418
|
+
|
|
1419
|
+
Check that invalid indices raise errors::
|
|
1420
|
+
|
|
1421
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
1422
|
+
sage: p = get_solver(solver='HiGHS')
|
|
1423
|
+
sage: p.variable_lower_bound(2)
|
|
1424
|
+
Traceback (most recent call last):
|
|
1425
|
+
...
|
|
1426
|
+
ValueError: invalid variable index 2
|
|
1427
|
+
sage: p.variable_lower_bound(-1)
|
|
1428
|
+
Traceback (most recent call last):
|
|
1429
|
+
...
|
|
1430
|
+
ValueError: invalid variable index -1
|
|
1431
|
+
sage: p.add_variable()
|
|
1432
|
+
0
|
|
1433
|
+
sage: p.variable_lower_bound(3, 5)
|
|
1434
|
+
Traceback (most recent call last):
|
|
1435
|
+
...
|
|
1436
|
+
ValueError: invalid variable index 3
|
|
1437
|
+
"""
|
|
1438
|
+
cdef double lb, ub
|
|
1439
|
+
cdef HighsInt status
|
|
1440
|
+
|
|
1441
|
+
if index < 0 or index >= self.numcols:
|
|
1442
|
+
raise ValueError(f"invalid variable index {index}")
|
|
1443
|
+
|
|
1444
|
+
if value is None:
|
|
1445
|
+
# Get current bound
|
|
1446
|
+
self._get_col_bounds(index, &lb, &ub)
|
|
1447
|
+
|
|
1448
|
+
if lb <= -Highs_getInfinity(self.highs) - 1:
|
|
1449
|
+
return None
|
|
1450
|
+
else:
|
|
1451
|
+
return lb
|
|
1452
|
+
else:
|
|
1453
|
+
# Set new bound
|
|
1454
|
+
self._get_col_bounds(index, &lb, &ub)
|
|
1455
|
+
|
|
1456
|
+
if value is None:
|
|
1457
|
+
lb = -Highs_getInfinity(self.highs)
|
|
1458
|
+
else:
|
|
1459
|
+
lb = float(value)
|
|
1460
|
+
|
|
1461
|
+
sig_on()
|
|
1462
|
+
status = Highs_changeColBounds(self.highs, index, lb, ub)
|
|
1463
|
+
sig_off()
|
|
1464
|
+
|
|
1465
|
+
if status != kHighsStatusOk:
|
|
1466
|
+
raise MIPSolverException("HiGHS: Failed to set variable lower bound")
|
|
1467
|
+
|
|
1468
|
+
cpdef col_name(self, int index):
|
|
1469
|
+
"""
|
|
1470
|
+
Return the ``index``-th column name.
|
|
1471
|
+
|
|
1472
|
+
INPUT:
|
|
1473
|
+
|
|
1474
|
+
- ``index`` -- integer; the column's id
|
|
1475
|
+
|
|
1476
|
+
EXAMPLES::
|
|
1477
|
+
|
|
1478
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
1479
|
+
sage: p = get_solver(solver='HiGHS')
|
|
1480
|
+
sage: p.add_variable(name='x')
|
|
1481
|
+
0
|
|
1482
|
+
sage: p.col_name(0)
|
|
1483
|
+
'x'
|
|
1484
|
+
"""
|
|
1485
|
+
if index < 0 or index >= self.numcols:
|
|
1486
|
+
raise ValueError(f"invalid column index {index}")
|
|
1487
|
+
return self.col_name_var.get(index, f"x_{index}")
|
|
1488
|
+
|
|
1489
|
+
cpdef col_bounds(self, int index):
|
|
1490
|
+
"""
|
|
1491
|
+
Return the bounds of a specific variable.
|
|
1492
|
+
|
|
1493
|
+
INPUT:
|
|
1494
|
+
|
|
1495
|
+
- ``index`` -- integer; the variable's id
|
|
1496
|
+
|
|
1497
|
+
OUTPUT:
|
|
1498
|
+
|
|
1499
|
+
A pair ``(lower_bound, upper_bound)``. Each of them can be set
|
|
1500
|
+
to ``None`` if the variable is not bounded in the
|
|
1501
|
+
corresponding direction, and is a real value otherwise.
|
|
1502
|
+
|
|
1503
|
+
EXAMPLES::
|
|
1504
|
+
|
|
1505
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
1506
|
+
sage: p = get_solver(solver='HiGHS')
|
|
1507
|
+
sage: p.add_variable()
|
|
1508
|
+
0
|
|
1509
|
+
sage: p.col_bounds(0)
|
|
1510
|
+
(0.0, None)
|
|
1511
|
+
sage: p.variable_upper_bound(0, 5)
|
|
1512
|
+
sage: p.col_bounds(0)
|
|
1513
|
+
(0.0, 5.0)
|
|
1514
|
+
"""
|
|
1515
|
+
cdef double lb, ub
|
|
1516
|
+
|
|
1517
|
+
if index < 0 or index >= self.numcols:
|
|
1518
|
+
raise ValueError(f"invalid variable index {index}")
|
|
1519
|
+
|
|
1520
|
+
self._get_col_bounds(index, &lb, &ub)
|
|
1521
|
+
|
|
1522
|
+
# Convert infinities to None
|
|
1523
|
+
if lb <= -Highs_getInfinity(self.highs) + 1:
|
|
1524
|
+
lb_ret = None
|
|
1525
|
+
else:
|
|
1526
|
+
lb_ret = lb
|
|
1527
|
+
|
|
1528
|
+
if ub >= Highs_getInfinity(self.highs) - 1:
|
|
1529
|
+
ub_ret = None
|
|
1530
|
+
else:
|
|
1531
|
+
ub_ret = ub
|
|
1532
|
+
|
|
1533
|
+
return (lb_ret, ub_ret)
|
|
1534
|
+
|
|
1535
|
+
cpdef bint is_variable_integer(self, int index) noexcept:
|
|
1536
|
+
"""
|
|
1537
|
+
Test whether the given variable is of integer type.
|
|
1538
|
+
|
|
1539
|
+
INPUT:
|
|
1540
|
+
|
|
1541
|
+
- ``index`` -- integer; the variable's id
|
|
1542
|
+
|
|
1543
|
+
EXAMPLES::
|
|
1544
|
+
|
|
1545
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
1546
|
+
sage: p = get_solver(solver='HiGHS')
|
|
1547
|
+
sage: p.add_variable()
|
|
1548
|
+
0
|
|
1549
|
+
sage: p.is_variable_integer(0)
|
|
1550
|
+
False
|
|
1551
|
+
sage: p.add_variable(integer=True)
|
|
1552
|
+
1
|
|
1553
|
+
sage: p.is_variable_integer(1)
|
|
1554
|
+
True
|
|
1555
|
+
|
|
1556
|
+
TESTS:
|
|
1557
|
+
|
|
1558
|
+
We check the behavior for an invalid index::
|
|
1559
|
+
|
|
1560
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
1561
|
+
sage: p = get_solver(solver='HiGHS')
|
|
1562
|
+
sage: p.is_variable_integer(2)
|
|
1563
|
+
False
|
|
1564
|
+
"""
|
|
1565
|
+
cdef HighsInt integrality, status
|
|
1566
|
+
|
|
1567
|
+
if index < 0 or index >= self.numcols:
|
|
1568
|
+
return False
|
|
1569
|
+
|
|
1570
|
+
status = Highs_getColIntegrality(self.highs, index, &integrality)
|
|
1571
|
+
if status != kHighsStatusOk:
|
|
1572
|
+
return False
|
|
1573
|
+
return integrality == kHighsVarTypeInteger
|
|
1574
|
+
|
|
1575
|
+
cpdef bint is_variable_binary(self, int index) noexcept:
|
|
1576
|
+
"""
|
|
1577
|
+
Test whether the given variable is of binary type.
|
|
1578
|
+
|
|
1579
|
+
INPUT:
|
|
1580
|
+
|
|
1581
|
+
- ``index`` -- integer; the variable's id
|
|
1582
|
+
|
|
1583
|
+
EXAMPLES::
|
|
1584
|
+
|
|
1585
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
1586
|
+
sage: p = get_solver(solver='HiGHS')
|
|
1587
|
+
sage: p.add_variable()
|
|
1588
|
+
0
|
|
1589
|
+
sage: p.is_variable_binary(0)
|
|
1590
|
+
False
|
|
1591
|
+
sage: p.add_variable(binary=True)
|
|
1592
|
+
1
|
|
1593
|
+
sage: p.is_variable_binary(1)
|
|
1594
|
+
True
|
|
1595
|
+
|
|
1596
|
+
TESTS:
|
|
1597
|
+
|
|
1598
|
+
We check the behavior for an invalid index::
|
|
1599
|
+
|
|
1600
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
1601
|
+
sage: p = get_solver(solver='HiGHS')
|
|
1602
|
+
sage: p.is_variable_binary(2)
|
|
1603
|
+
False
|
|
1604
|
+
"""
|
|
1605
|
+
cdef HighsInt integrality, status
|
|
1606
|
+
cdef double lb, ub
|
|
1607
|
+
|
|
1608
|
+
if index < 0 or index >= self.numcols:
|
|
1609
|
+
return False
|
|
1610
|
+
|
|
1611
|
+
status = Highs_getColIntegrality(self.highs, index, &integrality)
|
|
1612
|
+
if status != kHighsStatusOk:
|
|
1613
|
+
return False
|
|
1614
|
+
|
|
1615
|
+
if integrality == kHighsVarTypeInteger:
|
|
1616
|
+
self._get_col_bounds(index, &lb, &ub)
|
|
1617
|
+
return lb == 0.0 and ub == 1.0
|
|
1618
|
+
|
|
1619
|
+
return False
|
|
1620
|
+
|
|
1621
|
+
cpdef bint is_variable_continuous(self, int index) noexcept:
|
|
1622
|
+
"""
|
|
1623
|
+
Test whether the given variable is of continuous/real type.
|
|
1624
|
+
|
|
1625
|
+
INPUT:
|
|
1626
|
+
|
|
1627
|
+
- ``index`` -- integer; the variable's id
|
|
1628
|
+
|
|
1629
|
+
EXAMPLES::
|
|
1630
|
+
|
|
1631
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
1632
|
+
sage: p = get_solver(solver='HiGHS')
|
|
1633
|
+
sage: p.add_variable()
|
|
1634
|
+
0
|
|
1635
|
+
sage: p.is_variable_continuous(0)
|
|
1636
|
+
True
|
|
1637
|
+
|
|
1638
|
+
TESTS:
|
|
1639
|
+
|
|
1640
|
+
We check the behavior for an invalid index::
|
|
1641
|
+
|
|
1642
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
1643
|
+
sage: p = get_solver(solver='HiGHS')
|
|
1644
|
+
sage: p.is_variable_continuous(2)
|
|
1645
|
+
False
|
|
1646
|
+
"""
|
|
1647
|
+
cdef HighsInt integrality, status
|
|
1648
|
+
|
|
1649
|
+
if index < 0 or index >= self.numcols:
|
|
1650
|
+
return False
|
|
1651
|
+
|
|
1652
|
+
status = Highs_getColIntegrality(self.highs, index, &integrality)
|
|
1653
|
+
if status != kHighsStatusOk:
|
|
1654
|
+
return True # default to continuous
|
|
1655
|
+
return integrality == kHighsVarTypeContinuous
|
|
1656
|
+
|
|
1657
|
+
cpdef set_variable_type(self, int variable, int vtype):
|
|
1658
|
+
"""
|
|
1659
|
+
Set the type of a variable.
|
|
1660
|
+
|
|
1661
|
+
INPUT:
|
|
1662
|
+
|
|
1663
|
+
- ``variable`` -- integer; the variable's id
|
|
1664
|
+
|
|
1665
|
+
- ``vtype`` -- integer:
|
|
1666
|
+
|
|
1667
|
+
* `1` = Integer
|
|
1668
|
+
* `0` = Binary
|
|
1669
|
+
* `-1` = Real (Continuous)
|
|
1670
|
+
|
|
1671
|
+
EXAMPLES::
|
|
1672
|
+
|
|
1673
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
1674
|
+
sage: p = get_solver(solver='HiGHS')
|
|
1675
|
+
sage: p.add_variable()
|
|
1676
|
+
0
|
|
1677
|
+
sage: p.is_variable_continuous(0)
|
|
1678
|
+
True
|
|
1679
|
+
sage: p.set_variable_type(0, 1)
|
|
1680
|
+
sage: p.is_variable_integer(0)
|
|
1681
|
+
True
|
|
1682
|
+
|
|
1683
|
+
TESTS:
|
|
1684
|
+
|
|
1685
|
+
We sanity check the input that will be passed to HiGHS::
|
|
1686
|
+
|
|
1687
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
1688
|
+
sage: p = get_solver(solver='HiGHS')
|
|
1689
|
+
sage: p.set_variable_type(2, 0)
|
|
1690
|
+
Traceback (most recent call last):
|
|
1691
|
+
...
|
|
1692
|
+
ValueError: invalid variable index 2
|
|
1693
|
+
"""
|
|
1694
|
+
cdef HighsInt status
|
|
1695
|
+
cdef double lb, ub
|
|
1696
|
+
|
|
1697
|
+
if variable < 0 or variable >= self.numcols:
|
|
1698
|
+
raise ValueError(f"invalid variable index {variable}")
|
|
1699
|
+
|
|
1700
|
+
if vtype == 1:
|
|
1701
|
+
# Integer
|
|
1702
|
+
sig_on()
|
|
1703
|
+
status = Highs_changeColIntegrality(self.highs, variable, kHighsVarTypeInteger)
|
|
1704
|
+
sig_off()
|
|
1705
|
+
elif vtype == 0:
|
|
1706
|
+
# Binary - set to integer and change bounds to [0,1]
|
|
1707
|
+
sig_on()
|
|
1708
|
+
status = Highs_changeColIntegrality(self.highs, variable, kHighsVarTypeInteger)
|
|
1709
|
+
sig_off()
|
|
1710
|
+
|
|
1711
|
+
if status == kHighsStatusOk:
|
|
1712
|
+
sig_on()
|
|
1713
|
+
status = Highs_changeColBounds(self.highs, variable, 0.0, 1.0)
|
|
1714
|
+
sig_off()
|
|
1715
|
+
elif vtype == -1:
|
|
1716
|
+
# Continuous
|
|
1717
|
+
sig_on()
|
|
1718
|
+
status = Highs_changeColIntegrality(self.highs, variable, kHighsVarTypeContinuous)
|
|
1719
|
+
sig_off()
|
|
1720
|
+
else:
|
|
1721
|
+
raise ValueError(f"Unknown variable type {vtype}")
|
|
1722
|
+
|
|
1723
|
+
if status != kHighsStatusOk:
|
|
1724
|
+
raise MIPSolverException("HiGHS: Failed to set variable type")
|
|
1725
|
+
|
|
1726
|
+
cpdef row_bounds(self, int index):
|
|
1727
|
+
"""
|
|
1728
|
+
Return the bounds of a specific constraint.
|
|
1729
|
+
|
|
1730
|
+
INPUT:
|
|
1731
|
+
|
|
1732
|
+
- ``index`` -- integer; the constraint's id
|
|
1733
|
+
|
|
1734
|
+
OUTPUT:
|
|
1735
|
+
|
|
1736
|
+
A pair ``(lower_bound, upper_bound)``. Each of them can be set
|
|
1737
|
+
to ``None`` if the constraint is not bounded in the
|
|
1738
|
+
corresponding direction, and is a real value otherwise.
|
|
1739
|
+
|
|
1740
|
+
EXAMPLES::
|
|
1741
|
+
|
|
1742
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
1743
|
+
sage: p = get_solver(solver='HiGHS')
|
|
1744
|
+
sage: p.add_variables(5)
|
|
1745
|
+
4
|
|
1746
|
+
sage: p.add_linear_constraint(list(zip(range(5), range(5))), 2, 2)
|
|
1747
|
+
sage: p.row(0) # Note: zero coefficients are excluded in sparse format
|
|
1748
|
+
([1, 2, 3, 4], [1.0, 2.0, 3.0, 4.0])
|
|
1749
|
+
sage: p.row_bounds(0)
|
|
1750
|
+
(2.0, 2.0)
|
|
1751
|
+
"""
|
|
1752
|
+
cdef double lb, ub
|
|
1753
|
+
cdef double infinity
|
|
1754
|
+
|
|
1755
|
+
if index < 0 or index >= self.numrows:
|
|
1756
|
+
raise ValueError(f"invalid row index {index}")
|
|
1757
|
+
|
|
1758
|
+
self._get_row_bounds(index, &lb, &ub)
|
|
1759
|
+
infinity = Highs_getInfinity(self.highs)
|
|
1760
|
+
|
|
1761
|
+
return (
|
|
1762
|
+
(lb if abs(lb) < infinity else None),
|
|
1763
|
+
(ub if abs(ub) < infinity else None)
|
|
1764
|
+
)
|
|
1765
|
+
|
|
1766
|
+
cpdef row_name(self, int index):
|
|
1767
|
+
"""
|
|
1768
|
+
Return the ``index``-th row name.
|
|
1769
|
+
|
|
1770
|
+
INPUT:
|
|
1771
|
+
|
|
1772
|
+
- ``index`` -- integer; the row's id
|
|
1773
|
+
|
|
1774
|
+
EXAMPLES::
|
|
1775
|
+
|
|
1776
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
1777
|
+
sage: p = get_solver(solver='HiGHS')
|
|
1778
|
+
sage: p.add_linear_constraint([], 2, 2, name='foo')
|
|
1779
|
+
sage: p.row_name(0)
|
|
1780
|
+
'foo'
|
|
1781
|
+
"""
|
|
1782
|
+
if index < 0 or index >= self.numrows:
|
|
1783
|
+
raise ValueError(f"invalid row index {index}")
|
|
1784
|
+
|
|
1785
|
+
# Search for name in dictionary
|
|
1786
|
+
for name, idx in self.row_name_var.items():
|
|
1787
|
+
if idx == index:
|
|
1788
|
+
return str(name)
|
|
1789
|
+
|
|
1790
|
+
return f"constraint_{index}"
|
|
1791
|
+
|
|
1792
|
+
cpdef solver_parameter(self, name, value=None):
|
|
1793
|
+
"""
|
|
1794
|
+
Return or define a solver parameter.
|
|
1795
|
+
|
|
1796
|
+
INPUT:
|
|
1797
|
+
|
|
1798
|
+
- ``name`` -- string; the parameter name
|
|
1799
|
+
|
|
1800
|
+
- ``value`` -- the parameter's value if it is to be defined,
|
|
1801
|
+
or ``None`` (default) to obtain its current value
|
|
1802
|
+
|
|
1803
|
+
HiGHS solver parameters can be set using their option names as documented
|
|
1804
|
+
in the HiGHS documentation: https://ergo-code.github.io/HiGHS/dev/options/definitions/
|
|
1805
|
+
|
|
1806
|
+
Common parameters include:
|
|
1807
|
+
|
|
1808
|
+
- ``time_limit`` -- maximum time in seconds (double)
|
|
1809
|
+
- ``mip_rel_gap`` -- relative MIP gap tolerance (double)
|
|
1810
|
+
- ``mip_abs_gap`` -- absolute MIP gap tolerance (double)
|
|
1811
|
+
- ``threads`` -- number of threads to use (int)
|
|
1812
|
+
- ``presolve`` -- presolve option: "off", "choose", or "on"
|
|
1813
|
+
- ``solver`` -- solver to use: "choose", "simplex", "ipm", or "pdlp (need CUDA)"
|
|
1814
|
+
- ``parallel`` -- parallel option: "off", "choose", or "on"
|
|
1815
|
+
- ``log_to_console`` -- whether to log to console: True or False
|
|
1816
|
+
|
|
1817
|
+
EXAMPLES::
|
|
1818
|
+
|
|
1819
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
1820
|
+
sage: p = get_solver(solver='HiGHS')
|
|
1821
|
+
sage: p.solver_parameter("time_limit", 60)
|
|
1822
|
+
sage: p.solver_parameter("time_limit")
|
|
1823
|
+
60.0
|
|
1824
|
+
sage: p.solver_parameter("threads", 2)
|
|
1825
|
+
sage: p.solver_parameter("threads")
|
|
1826
|
+
2
|
|
1827
|
+
sage: p.solver_parameter("presolve", "on")
|
|
1828
|
+
sage: p.solver_parameter("presolve")
|
|
1829
|
+
'on'
|
|
1830
|
+
|
|
1831
|
+
You can also use boolean values for options::
|
|
1832
|
+
|
|
1833
|
+
sage: p.solver_parameter("log_to_console", False)
|
|
1834
|
+
sage: p.solver_parameter("log_to_console")
|
|
1835
|
+
False
|
|
1836
|
+
|
|
1837
|
+
Float parameters like MIP gap tolerance work correctly::
|
|
1838
|
+
|
|
1839
|
+
sage: p.solver_parameter("mip_rel_gap", 0.05)
|
|
1840
|
+
sage: p.solver_parameter("mip_rel_gap")
|
|
1841
|
+
0.05
|
|
1842
|
+
"""
|
|
1843
|
+
cdef HighsInt status
|
|
1844
|
+
cdef bytes name_bytes
|
|
1845
|
+
cdef HighsInt int_value
|
|
1846
|
+
cdef double double_value
|
|
1847
|
+
cdef HighsInt bool_value
|
|
1848
|
+
cdef char* str_value
|
|
1849
|
+
cdef HighsInt option_type
|
|
1850
|
+
|
|
1851
|
+
name_bytes = str(name).encode('utf-8')
|
|
1852
|
+
|
|
1853
|
+
if value is None:
|
|
1854
|
+
# Get parameter - try each type until one succeeds
|
|
1855
|
+
# Try bool first (most common for flags, and avoids HiGHS error messages)
|
|
1856
|
+
status = Highs_getBoolOptionValue(self.highs, name_bytes, &bool_value)
|
|
1857
|
+
if status == kHighsStatusOk:
|
|
1858
|
+
return bool(bool_value)
|
|
1859
|
+
|
|
1860
|
+
# Try int
|
|
1861
|
+
status = Highs_getIntOptionValue(self.highs, name_bytes, &int_value)
|
|
1862
|
+
if status == kHighsStatusOk:
|
|
1863
|
+
return int_value
|
|
1864
|
+
|
|
1865
|
+
# Try double
|
|
1866
|
+
status = Highs_getDoubleOptionValue(self.highs, name_bytes, &double_value)
|
|
1867
|
+
if status == kHighsStatusOk:
|
|
1868
|
+
return double_value
|
|
1869
|
+
|
|
1870
|
+
# Try string (allocate buffer for string value)
|
|
1871
|
+
str_value = <char*> malloc(256 * sizeof(char))
|
|
1872
|
+
if str_value == NULL:
|
|
1873
|
+
raise MemoryError("Failed to allocate memory for string option")
|
|
1874
|
+
try:
|
|
1875
|
+
status = Highs_getStringOptionValue(self.highs, name_bytes, str_value)
|
|
1876
|
+
if status == kHighsStatusOk:
|
|
1877
|
+
result = str_value.decode('utf-8')
|
|
1878
|
+
return result
|
|
1879
|
+
else:
|
|
1880
|
+
raise ValueError(f"Unknown option {name}")
|
|
1881
|
+
finally:
|
|
1882
|
+
free(str_value)
|
|
1883
|
+
else:
|
|
1884
|
+
# Set parameter - need to determine type
|
|
1885
|
+
# Convert Sage types to Python types for easier type checking
|
|
1886
|
+
try:
|
|
1887
|
+
# Try to convert to Python numeric type if it's a Sage type
|
|
1888
|
+
# Check for float first since int() would truncate floats
|
|
1889
|
+
if isinstance(value, float):
|
|
1890
|
+
pass # Already a Python float
|
|
1891
|
+
elif hasattr(value, '__float__') and not isinstance(value, (bool, str, int)):
|
|
1892
|
+
value = float(value)
|
|
1893
|
+
elif hasattr(value, '__int__') and not isinstance(value, (bool, str, float)):
|
|
1894
|
+
value = int(value)
|
|
1895
|
+
except (TypeError, AttributeError):
|
|
1896
|
+
pass
|
|
1897
|
+
|
|
1898
|
+
# Check bool first since bool is a subclass of int in Python
|
|
1899
|
+
if isinstance(value, bool):
|
|
1900
|
+
status = Highs_setBoolOptionValue(self.highs, name_bytes, value)
|
|
1901
|
+
elif isinstance(value, str):
|
|
1902
|
+
value_bytes = value.encode('utf-8')
|
|
1903
|
+
status = Highs_setStringOptionValue(self.highs, name_bytes, value_bytes)
|
|
1904
|
+
elif isinstance(value, (int, float)):
|
|
1905
|
+
# Try as double first (works for both int and float)
|
|
1906
|
+
status = Highs_setDoubleOptionValue(self.highs, name_bytes, float(value))
|
|
1907
|
+
if status != kHighsStatusOk and isinstance(value, int):
|
|
1908
|
+
# If double failed and it's an int, try as int
|
|
1909
|
+
status = Highs_setIntOptionValue(self.highs, name_bytes, value)
|
|
1910
|
+
if status != kHighsStatusOk:
|
|
1911
|
+
# If both numeric methods failed, try as string (works for advanced options)
|
|
1912
|
+
# For floats that are whole numbers, convert to int string to avoid "4.0" format
|
|
1913
|
+
if isinstance(value, float) and value == int(value):
|
|
1914
|
+
value_bytes = str(int(value)).encode('utf-8')
|
|
1915
|
+
else:
|
|
1916
|
+
value_bytes = str(value).encode('utf-8')
|
|
1917
|
+
status = Highs_setStringOptionValue(self.highs, name_bytes, value_bytes)
|
|
1918
|
+
else:
|
|
1919
|
+
raise ValueError(f"Unknown parameter type for {name}: {type(value)}")
|
|
1920
|
+
|
|
1921
|
+
if status != kHighsStatusOk:
|
|
1922
|
+
raise MIPSolverException(f"HiGHS: Failed to set parameter {name}")
|
|
1923
|
+
|
|
1924
|
+
cpdef write_lp(self, filename):
|
|
1925
|
+
"""
|
|
1926
|
+
Write the problem to a .lp file.
|
|
1927
|
+
|
|
1928
|
+
INPUT:
|
|
1929
|
+
|
|
1930
|
+
- ``filename`` -- string; the file name
|
|
1931
|
+
|
|
1932
|
+
EXAMPLES::
|
|
1933
|
+
|
|
1934
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
1935
|
+
sage: p = get_solver(solver='HiGHS')
|
|
1936
|
+
sage: p.add_variables(2)
|
|
1937
|
+
1
|
|
1938
|
+
sage: p.add_linear_constraint([(0, 1), (1, 1)], None, 2.0)
|
|
1939
|
+
sage: import tempfile
|
|
1940
|
+
sage: with tempfile.NamedTemporaryFile(suffix='.lp') as f:
|
|
1941
|
+
....: p.write_lp(f.name)
|
|
1942
|
+
"""
|
|
1943
|
+
cdef bytes filename_bytes
|
|
1944
|
+
cdef HighsInt status
|
|
1945
|
+
|
|
1946
|
+
import os
|
|
1947
|
+
cdef str filenamestr = str(filename)
|
|
1948
|
+
cdef object _root
|
|
1949
|
+
cdef str ext
|
|
1950
|
+
|
|
1951
|
+
_root, ext = os.path.splitext(filenamestr)
|
|
1952
|
+
if ext.lower() != '.lp':
|
|
1953
|
+
filenamestr = filenamestr + '.lp'
|
|
1954
|
+
|
|
1955
|
+
filename_bytes = os.fsencode(filenamestr)
|
|
1956
|
+
|
|
1957
|
+
sig_on()
|
|
1958
|
+
status = Highs_writeModel(self.highs, filename_bytes)
|
|
1959
|
+
sig_off()
|
|
1960
|
+
|
|
1961
|
+
if status != kHighsStatusOk and status != kHighsStatusWarning:
|
|
1962
|
+
raise MIPSolverException(f"HiGHS: Failed to write LP file {filenamestr} (status {status})")
|
|
1963
|
+
|
|
1964
|
+
cpdef write_mps(self, filename, int modern):
|
|
1965
|
+
"""
|
|
1966
|
+
Write the problem to a .mps file.
|
|
1967
|
+
|
|
1968
|
+
INPUT:
|
|
1969
|
+
|
|
1970
|
+
- ``filename`` -- string; the file name
|
|
1971
|
+
- ``modern`` -- integer; whether to use modern MPS format (ignored for HiGHS)
|
|
1972
|
+
|
|
1973
|
+
.. NOTE::
|
|
1974
|
+
|
|
1975
|
+
HiGHS determines the output format from the filename extension.
|
|
1976
|
+
The ``modern`` flag is accepted for API compatibility but ignored.
|
|
1977
|
+
|
|
1978
|
+
EXAMPLES::
|
|
1979
|
+
|
|
1980
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
1981
|
+
sage: p = get_solver(solver='HiGHS')
|
|
1982
|
+
sage: p.add_variables(2)
|
|
1983
|
+
1
|
|
1984
|
+
sage: p.add_linear_constraint([(0, 1), (1, 1)], None, 2.0)
|
|
1985
|
+
sage: import tempfile
|
|
1986
|
+
sage: with tempfile.NamedTemporaryFile(suffix='.mps') as f:
|
|
1987
|
+
....: p.write_mps(f.name, 1)
|
|
1988
|
+
"""
|
|
1989
|
+
cdef bytes filename_bytes
|
|
1990
|
+
cdef HighsInt status
|
|
1991
|
+
|
|
1992
|
+
import os
|
|
1993
|
+
cdef str filenamestr = str(filename)
|
|
1994
|
+
cdef object _root
|
|
1995
|
+
cdef str ext
|
|
1996
|
+
|
|
1997
|
+
_root, ext = os.path.splitext(filenamestr)
|
|
1998
|
+
if ext.lower() != '.mps':
|
|
1999
|
+
filenamestr = filenamestr + '.mps'
|
|
2000
|
+
|
|
2001
|
+
filename_bytes = os.fsencode(filenamestr)
|
|
2002
|
+
|
|
2003
|
+
sig_on()
|
|
2004
|
+
status = Highs_writeModel(self.highs, filename_bytes)
|
|
2005
|
+
sig_off()
|
|
2006
|
+
|
|
2007
|
+
if status != kHighsStatusOk and status != kHighsStatusWarning:
|
|
2008
|
+
raise MIPSolverException(f"HiGHS: Failed to write MPS file {filenamestr} (status {status})")
|
|
2009
|
+
|
|
2010
|
+
cpdef remove_constraint(self, int i):
|
|
2011
|
+
"""
|
|
2012
|
+
Remove a constraint from ``self``.
|
|
2013
|
+
|
|
2014
|
+
INPUT:
|
|
2015
|
+
|
|
2016
|
+
- ``i`` -- index of the constraint to remove
|
|
2017
|
+
|
|
2018
|
+
EXAMPLES::
|
|
2019
|
+
|
|
2020
|
+
sage: p = MixedIntegerLinearProgram(solver='HiGHS')
|
|
2021
|
+
sage: x, y = p['x'], p['y']
|
|
2022
|
+
sage: p.add_constraint(2*x + 3*y <= 6)
|
|
2023
|
+
sage: p.add_constraint(3*x + 2*y <= 6)
|
|
2024
|
+
sage: p.add_constraint(x >= 0)
|
|
2025
|
+
sage: p.set_objective(x + y + 7)
|
|
2026
|
+
sage: p.set_integer(x); p.set_integer(y)
|
|
2027
|
+
sage: p.solve()
|
|
2028
|
+
9.0
|
|
2029
|
+
sage: p.remove_constraint(0)
|
|
2030
|
+
sage: p.solve()
|
|
2031
|
+
10.0
|
|
2032
|
+
|
|
2033
|
+
Removing fancy constraints does not make Sage crash::
|
|
2034
|
+
|
|
2035
|
+
sage: MixedIntegerLinearProgram(solver = "HiGHS").remove_constraint(-2)
|
|
2036
|
+
Traceback (most recent call last):
|
|
2037
|
+
...
|
|
2038
|
+
ValueError: The constraint's index i must satisfy 0 <= i < number_of_constraints
|
|
2039
|
+
"""
|
|
2040
|
+
cdef HighsInt status
|
|
2041
|
+
|
|
2042
|
+
if i < 0 or i >= self.numrows:
|
|
2043
|
+
raise ValueError("The constraint's index i must satisfy 0 <= i < number_of_constraints")
|
|
2044
|
+
|
|
2045
|
+
# Delete the single row
|
|
2046
|
+
sig_on()
|
|
2047
|
+
status = Highs_deleteRowsByRange(self.highs, i, i)
|
|
2048
|
+
sig_off()
|
|
2049
|
+
|
|
2050
|
+
if status != kHighsStatusOk:
|
|
2051
|
+
raise MIPSolverException("HiGHS: Failed to remove constraint")
|
|
2052
|
+
|
|
2053
|
+
self.numrows -= 1
|
|
2054
|
+
|
|
2055
|
+
# Update row name mapping
|
|
2056
|
+
names_to_update = {}
|
|
2057
|
+
names_to_remove = []
|
|
2058
|
+
for name, row_idx in self.row_name_var.items():
|
|
2059
|
+
if row_idx < i:
|
|
2060
|
+
names_to_update[name] = row_idx
|
|
2061
|
+
elif row_idx > i:
|
|
2062
|
+
names_to_update[name] = row_idx - 1
|
|
2063
|
+
else:
|
|
2064
|
+
names_to_remove.append(name)
|
|
2065
|
+
|
|
2066
|
+
for name in names_to_remove:
|
|
2067
|
+
del self.row_name_var[name]
|
|
2068
|
+
self.row_name_var.update(names_to_update)
|
|
2069
|
+
|
|
2070
|
+
cpdef remove_constraints(self, constraints):
|
|
2071
|
+
"""
|
|
2072
|
+
Remove several constraints.
|
|
2073
|
+
|
|
2074
|
+
INPUT:
|
|
2075
|
+
|
|
2076
|
+
- ``constraints`` -- an iterable containing the indices of the rows to remove
|
|
2077
|
+
|
|
2078
|
+
EXAMPLES::
|
|
2079
|
+
|
|
2080
|
+
sage: p = MixedIntegerLinearProgram(solver='HiGHS')
|
|
2081
|
+
sage: x, y = p['x'], p['y']
|
|
2082
|
+
sage: p.add_constraint(2*x + 3*y <= 6)
|
|
2083
|
+
sage: p.add_constraint(3*x + 2*y <= 6)
|
|
2084
|
+
sage: p.add_constraint(x >= 0)
|
|
2085
|
+
sage: p.set_objective(x + y + 7)
|
|
2086
|
+
sage: p.set_integer(x); p.set_integer(y)
|
|
2087
|
+
sage: p.solve()
|
|
2088
|
+
9.0
|
|
2089
|
+
sage: p.remove_constraints([0])
|
|
2090
|
+
sage: p.solve()
|
|
2091
|
+
10.0
|
|
2092
|
+
sage: p.get_values([x,y])
|
|
2093
|
+
[-0.0, 3.0]
|
|
2094
|
+
|
|
2095
|
+
TESTS:
|
|
2096
|
+
|
|
2097
|
+
Removing fancy constraints does not make Sage crash::
|
|
2098
|
+
|
|
2099
|
+
sage: MixedIntegerLinearProgram(solver="HiGHS").remove_constraints([0, -2])
|
|
2100
|
+
Traceback (most recent call last):
|
|
2101
|
+
...
|
|
2102
|
+
ValueError: The constraint's index i must satisfy 0 <= i < number_of_constraints
|
|
2103
|
+
"""
|
|
2104
|
+
if isinstance(constraints, int):
|
|
2105
|
+
self.remove_constraint(constraints)
|
|
2106
|
+
return
|
|
2107
|
+
|
|
2108
|
+
cdef int last = self.nrows() + 1
|
|
2109
|
+
|
|
2110
|
+
for c in sorted(constraints, reverse=True):
|
|
2111
|
+
if c != last:
|
|
2112
|
+
self.remove_constraint(c)
|
|
2113
|
+
last = c
|
|
2114
|
+
|
|
2115
|
+
cpdef row(self, int index):
|
|
2116
|
+
"""
|
|
2117
|
+
Return the ``index``-th constraint as a pair of lists.
|
|
2118
|
+
|
|
2119
|
+
INPUT:
|
|
2120
|
+
|
|
2121
|
+
- ``index`` -- index of the constraint
|
|
2122
|
+
|
|
2123
|
+
OUTPUT:
|
|
2124
|
+
|
|
2125
|
+
A pair ``(indices, coeffs)`` where ``indices`` lists the
|
|
2126
|
+
entries whose coefficient is nonzero, and to which ``coeffs``
|
|
2127
|
+
associates their coefficient in the order of `indices`.
|
|
2128
|
+
|
|
2129
|
+
EXAMPLES::
|
|
2130
|
+
|
|
2131
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
2132
|
+
sage: p = get_solver(solver='HiGHS')
|
|
2133
|
+
sage: p.add_variables(5)
|
|
2134
|
+
4
|
|
2135
|
+
sage: p.add_linear_constraint(list(zip(range(5), range(5))), 2, 2)
|
|
2136
|
+
sage: p.row(0) # Note: zero coefficients are excluded in sparse format
|
|
2137
|
+
([1, 2, 3, 4], [1.0, 2.0, 3.0, 4.0])
|
|
2138
|
+
sage: p.row(1)
|
|
2139
|
+
Traceback (most recent call last):
|
|
2140
|
+
...
|
|
2141
|
+
ValueError: invalid row index 1
|
|
2142
|
+
"""
|
|
2143
|
+
cdef HighsInt num_row, num_nz, matrix_start
|
|
2144
|
+
cdef HighsInt* matrix_index
|
|
2145
|
+
cdef double* matrix_value
|
|
2146
|
+
cdef double lb, ub
|
|
2147
|
+
cdef HighsInt status
|
|
2148
|
+
cdef list indices = []
|
|
2149
|
+
cdef list coeffs = []
|
|
2150
|
+
cdef int j
|
|
2151
|
+
|
|
2152
|
+
if index < 0 or index >= self.numrows:
|
|
2153
|
+
raise ValueError(f"invalid row index {index}")
|
|
2154
|
+
|
|
2155
|
+
# First call: get the number of non-zeros
|
|
2156
|
+
sig_on()
|
|
2157
|
+
status = Highs_getRowsByRange(self.highs, index, index,
|
|
2158
|
+
&num_row, &lb, &ub, &num_nz,
|
|
2159
|
+
NULL, NULL, NULL)
|
|
2160
|
+
sig_off()
|
|
2161
|
+
|
|
2162
|
+
if status != kHighsStatusOk:
|
|
2163
|
+
raise MIPSolverException("HiGHS: Failed to get row info")
|
|
2164
|
+
|
|
2165
|
+
if num_nz == 0:
|
|
2166
|
+
return ([], [])
|
|
2167
|
+
|
|
2168
|
+
# Allocate space for the matrix data
|
|
2169
|
+
matrix_index = <HighsInt*> malloc(num_nz * sizeof(HighsInt))
|
|
2170
|
+
matrix_value = <double*> malloc(num_nz * sizeof(double))
|
|
2171
|
+
|
|
2172
|
+
if matrix_index == NULL or matrix_value == NULL:
|
|
2173
|
+
free(matrix_index)
|
|
2174
|
+
free(matrix_value)
|
|
2175
|
+
raise MemoryError("Failed to allocate memory")
|
|
2176
|
+
|
|
2177
|
+
try:
|
|
2178
|
+
# Second call: get the actual matrix data
|
|
2179
|
+
sig_on()
|
|
2180
|
+
status = Highs_getRowsByRange(self.highs, index, index,
|
|
2181
|
+
&num_row, &lb, &ub, &num_nz,
|
|
2182
|
+
&matrix_start, matrix_index, matrix_value)
|
|
2183
|
+
sig_off()
|
|
2184
|
+
|
|
2185
|
+
if status != kHighsStatusOk:
|
|
2186
|
+
raise MIPSolverException("HiGHS: Failed to get row data")
|
|
2187
|
+
|
|
2188
|
+
# Extract indices and coefficients
|
|
2189
|
+
for j in range(num_nz):
|
|
2190
|
+
indices.append(matrix_index[j])
|
|
2191
|
+
coeffs.append(matrix_value[j])
|
|
2192
|
+
finally:
|
|
2193
|
+
free(matrix_index)
|
|
2194
|
+
free(matrix_value)
|
|
2195
|
+
|
|
2196
|
+
return (indices, coeffs)
|
|
2197
|
+
|
|
2198
|
+
cpdef add_col(self, indices, coeffs):
|
|
2199
|
+
"""
|
|
2200
|
+
Add a column.
|
|
2201
|
+
|
|
2202
|
+
INPUT:
|
|
2203
|
+
|
|
2204
|
+
- ``indices`` -- list of integers; this list contains the
|
|
2205
|
+
indices of the constraints in which the variable's
|
|
2206
|
+
coefficient is nonzero
|
|
2207
|
+
|
|
2208
|
+
- ``coeffs`` -- list of real values; associates a coefficient
|
|
2209
|
+
to the variable in each of the constraints in which it
|
|
2210
|
+
appears. Namely, the i-th entry of ``coeffs`` corresponds to
|
|
2211
|
+
the coefficient of the variable in the constraint
|
|
2212
|
+
represented by the i-th entry in ``indices``.
|
|
2213
|
+
|
|
2214
|
+
.. NOTE::
|
|
2215
|
+
|
|
2216
|
+
``indices`` and ``coeffs`` are expected to be of the same
|
|
2217
|
+
length.
|
|
2218
|
+
|
|
2219
|
+
EXAMPLES::
|
|
2220
|
+
|
|
2221
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
2222
|
+
sage: p = get_solver(solver='HiGHS')
|
|
2223
|
+
sage: p.ncols()
|
|
2224
|
+
0
|
|
2225
|
+
sage: p.nrows()
|
|
2226
|
+
0
|
|
2227
|
+
sage: p.add_linear_constraints(5, 0, None)
|
|
2228
|
+
sage: p.add_col(list(range(5)), list(range(5)))
|
|
2229
|
+
sage: p.nrows()
|
|
2230
|
+
5
|
|
2231
|
+
"""
|
|
2232
|
+
cdef int col_idx
|
|
2233
|
+
cdef int i, constraint_idx
|
|
2234
|
+
cdef HighsInt status
|
|
2235
|
+
|
|
2236
|
+
# Add a new column (variable)
|
|
2237
|
+
col_idx = self.add_variable(lower_bound=0.0, upper_bound=None)
|
|
2238
|
+
|
|
2239
|
+
# Set the coefficients for this column in the existing constraints
|
|
2240
|
+
for i, constraint_idx in enumerate(indices):
|
|
2241
|
+
if constraint_idx < 0 or constraint_idx >= self.nrows():
|
|
2242
|
+
continue
|
|
2243
|
+
|
|
2244
|
+
coeff = coeffs[i]
|
|
2245
|
+
if coeff != 0:
|
|
2246
|
+
sig_on()
|
|
2247
|
+
status = Highs_changeCoeff(self.highs, constraint_idx, col_idx, float(coeff))
|
|
2248
|
+
sig_off()
|
|
2249
|
+
|
|
2250
|
+
if status != kHighsStatusOk:
|
|
2251
|
+
raise MIPSolverException("HiGHS: Failed to set coefficient")
|
|
2252
|
+
|
|
2253
|
+
cpdef int get_row_stat(self, int i) except? -1:
|
|
2254
|
+
"""
|
|
2255
|
+
Retrieve the status of a constraint.
|
|
2256
|
+
|
|
2257
|
+
INPUT:
|
|
2258
|
+
|
|
2259
|
+
- ``i`` -- the index of the constraint
|
|
2260
|
+
|
|
2261
|
+
OUTPUT:
|
|
2262
|
+
|
|
2263
|
+
Current status assigned to the auxiliary variable associated with the i-th row:
|
|
2264
|
+
|
|
2265
|
+
* 0 kLower: non-basic variable at lower bound
|
|
2266
|
+
* 1 kBasic: basic variable
|
|
2267
|
+
* 2 kUpper: non-basic variable at upper bound
|
|
2268
|
+
* 3 kZero: non-basic free variable at zero
|
|
2269
|
+
* 4 kNonbasic: nonbasic (used for unbounded variables)
|
|
2270
|
+
|
|
2271
|
+
EXAMPLES::
|
|
2272
|
+
|
|
2273
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
2274
|
+
sage: lp = get_solver(solver='HiGHS')
|
|
2275
|
+
sage: lp.add_variables(3)
|
|
2276
|
+
2
|
|
2277
|
+
sage: lp.add_linear_constraint(list(zip([0, 1, 2], [8, 6, 1])), None, 48)
|
|
2278
|
+
sage: lp.add_linear_constraint(list(zip([0, 1, 2], [4, 2, 1.5])), None, 20)
|
|
2279
|
+
sage: lp.add_linear_constraint(list(zip([0, 1, 2], [2, 1.5, 0.5])), None, 8)
|
|
2280
|
+
sage: lp.set_objective([60, 30, 20])
|
|
2281
|
+
sage: lp.solve()
|
|
2282
|
+
0
|
|
2283
|
+
sage: lp.get_row_stat(0) # doctest: +SKIP
|
|
2284
|
+
2
|
|
2285
|
+
sage: lp.get_row_stat(1) # doctest: +SKIP
|
|
2286
|
+
2
|
|
2287
|
+
sage: lp.get_row_stat(-1)
|
|
2288
|
+
Traceback (most recent call last):
|
|
2289
|
+
...
|
|
2290
|
+
ValueError: The constraint's index i must satisfy 0 <= i < number_of_constraints
|
|
2291
|
+
"""
|
|
2292
|
+
cdef HighsInt* row_status
|
|
2293
|
+
cdef HighsInt* col_status
|
|
2294
|
+
cdef HighsInt num_rows, num_cols
|
|
2295
|
+
cdef HighsInt status
|
|
2296
|
+
cdef HighsInt result
|
|
2297
|
+
|
|
2298
|
+
if i < 0 or i >= self.numrows:
|
|
2299
|
+
raise ValueError("The constraint's index i must satisfy 0 <= i < number_of_constraints")
|
|
2300
|
+
|
|
2301
|
+
# Note: HiGHS C API doesn't have getBasisValidity function
|
|
2302
|
+
# We'll try to get the basis and handle errors if no basis exists
|
|
2303
|
+
|
|
2304
|
+
num_rows = Highs_getNumRow(self.highs)
|
|
2305
|
+
num_cols = Highs_getNumCol(self.highs)
|
|
2306
|
+
|
|
2307
|
+
row_status = <HighsInt*> malloc(num_rows * sizeof(HighsInt))
|
|
2308
|
+
col_status = <HighsInt*> malloc(num_cols * sizeof(HighsInt))
|
|
2309
|
+
|
|
2310
|
+
if row_status == NULL or col_status == NULL:
|
|
2311
|
+
free(row_status)
|
|
2312
|
+
free(col_status)
|
|
2313
|
+
raise MemoryError("Failed to allocate memory")
|
|
2314
|
+
|
|
2315
|
+
try:
|
|
2316
|
+
sig_on()
|
|
2317
|
+
status = Highs_getBasis(self.highs, col_status, row_status)
|
|
2318
|
+
sig_off()
|
|
2319
|
+
|
|
2320
|
+
if status != kHighsStatusOk:
|
|
2321
|
+
raise MIPSolverException("HiGHS: Failed to get basis")
|
|
2322
|
+
|
|
2323
|
+
result = row_status[i]
|
|
2324
|
+
finally:
|
|
2325
|
+
free(row_status)
|
|
2326
|
+
free(col_status)
|
|
2327
|
+
|
|
2328
|
+
return result
|
|
2329
|
+
|
|
2330
|
+
cpdef int get_col_stat(self, int j) except? -1:
|
|
2331
|
+
"""
|
|
2332
|
+
Retrieve the status of a variable.
|
|
2333
|
+
|
|
2334
|
+
INPUT:
|
|
2335
|
+
|
|
2336
|
+
- ``j`` -- the index of the variable
|
|
2337
|
+
|
|
2338
|
+
OUTPUT:
|
|
2339
|
+
|
|
2340
|
+
Current status assigned to the structural variable associated with the j-th column:
|
|
2341
|
+
|
|
2342
|
+
* 0 kLower: non-basic variable at lower bound
|
|
2343
|
+
* 1 kBasic: basic variable
|
|
2344
|
+
* 2 kUpper: non-basic variable at upper bound
|
|
2345
|
+
* 3 kZero: non-basic free variable at zero
|
|
2346
|
+
* 4 kNonbasic: nonbasic (used for unbounded variables)
|
|
2347
|
+
|
|
2348
|
+
EXAMPLES::
|
|
2349
|
+
|
|
2350
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
2351
|
+
sage: lp = get_solver(solver='HiGHS')
|
|
2352
|
+
sage: lp.add_variables(3)
|
|
2353
|
+
2
|
|
2354
|
+
sage: lp.add_linear_constraint(list(zip([0, 1, 2], [8, 6, 1])), None, 48)
|
|
2355
|
+
sage: lp.add_linear_constraint(list(zip([0, 1, 2], [4, 2, 1.5])), None, 20)
|
|
2356
|
+
sage: lp.add_linear_constraint(list(zip([0, 1, 2], [2, 1.5, 0.5])), None, 8)
|
|
2357
|
+
sage: lp.set_objective([60, 30, 20])
|
|
2358
|
+
sage: lp.solve()
|
|
2359
|
+
0
|
|
2360
|
+
sage: lp.get_col_stat(0)
|
|
2361
|
+
1
|
|
2362
|
+
sage: lp.get_col_stat(1)
|
|
2363
|
+
0
|
|
2364
|
+
sage: lp.get_col_stat(100)
|
|
2365
|
+
Traceback (most recent call last):
|
|
2366
|
+
...
|
|
2367
|
+
ValueError: The variable's index j must satisfy 0 <= j < number_of_variables
|
|
2368
|
+
"""
|
|
2369
|
+
cdef HighsInt* col_status
|
|
2370
|
+
cdef HighsInt* row_status
|
|
2371
|
+
cdef HighsInt num_cols, num_rows
|
|
2372
|
+
cdef HighsInt status
|
|
2373
|
+
cdef HighsInt result
|
|
2374
|
+
|
|
2375
|
+
if j < 0 or j >= self.numcols:
|
|
2376
|
+
raise ValueError("The variable's index j must satisfy 0 <= j < number_of_variables")
|
|
2377
|
+
|
|
2378
|
+
# Note: HiGHS C API doesn't have getBasisValidity function
|
|
2379
|
+
# We'll try to get the basis and handle errors if no basis exists
|
|
2380
|
+
|
|
2381
|
+
num_cols = Highs_getNumCol(self.highs)
|
|
2382
|
+
num_rows = Highs_getNumRow(self.highs)
|
|
2383
|
+
|
|
2384
|
+
col_status = <HighsInt*> malloc(num_cols * sizeof(HighsInt))
|
|
2385
|
+
row_status = <HighsInt*> malloc(num_rows * sizeof(HighsInt))
|
|
2386
|
+
|
|
2387
|
+
if col_status == NULL or row_status == NULL:
|
|
2388
|
+
free(col_status)
|
|
2389
|
+
free(row_status)
|
|
2390
|
+
raise MemoryError("Failed to allocate memory")
|
|
2391
|
+
|
|
2392
|
+
try:
|
|
2393
|
+
sig_on()
|
|
2394
|
+
status = Highs_getBasis(self.highs, col_status, row_status)
|
|
2395
|
+
sig_off()
|
|
2396
|
+
|
|
2397
|
+
if status != kHighsStatusOk:
|
|
2398
|
+
raise MIPSolverException("HiGHS: Failed to get basis")
|
|
2399
|
+
|
|
2400
|
+
result = col_status[j]
|
|
2401
|
+
finally:
|
|
2402
|
+
free(col_status)
|
|
2403
|
+
free(row_status)
|
|
2404
|
+
|
|
2405
|
+
return result
|
|
2406
|
+
|
|
2407
|
+
cpdef set_row_stat(self, int i, int stat):
|
|
2408
|
+
"""
|
|
2409
|
+
Set the status of a constraint.
|
|
2410
|
+
|
|
2411
|
+
INPUT:
|
|
2412
|
+
|
|
2413
|
+
- ``i`` -- the index of the constraint
|
|
2414
|
+
|
|
2415
|
+
- ``stat`` -- the status to set to:
|
|
2416
|
+
|
|
2417
|
+
* 0 kLower: non-basic variable at lower bound
|
|
2418
|
+
* 1 kBasic: basic variable
|
|
2419
|
+
* 2 kUpper: non-basic variable at upper bound
|
|
2420
|
+
* 3 kZero: non-basic free variable at zero
|
|
2421
|
+
* 4 kNonbasic: nonbasic (used for unbounded variables)
|
|
2422
|
+
|
|
2423
|
+
.. NOTE::
|
|
2424
|
+
|
|
2425
|
+
HiGHS may reject invalid basis configurations. Setting arbitrary
|
|
2426
|
+
status values may result in the basis being rejected and the
|
|
2427
|
+
original basis being preserved.
|
|
2428
|
+
|
|
2429
|
+
EXAMPLES::
|
|
2430
|
+
|
|
2431
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
2432
|
+
sage: lp = get_solver(solver='HiGHS')
|
|
2433
|
+
sage: lp.add_variables(3)
|
|
2434
|
+
2
|
|
2435
|
+
sage: lp.add_linear_constraint(list(zip([0, 1, 2], [8, 6, 1])), None, 48)
|
|
2436
|
+
sage: lp.add_linear_constraint(list(zip([0, 1, 2], [4, 2, 1.5])), None, 20)
|
|
2437
|
+
sage: lp.add_linear_constraint(list(zip([0, 1, 2], [2, 1.5, 0.5])), None, 8)
|
|
2438
|
+
sage: lp.set_objective([60, 30, 20])
|
|
2439
|
+
sage: lp.solve()
|
|
2440
|
+
0
|
|
2441
|
+
sage: lp.get_row_stat(0)
|
|
2442
|
+
1
|
|
2443
|
+
sage: lp.set_col_stat(0, 2)
|
|
2444
|
+
sage: lp.get_col_stat(0)
|
|
2445
|
+
2
|
|
2446
|
+
sage: lp.set_row_stat(0, 3)
|
|
2447
|
+
sage: lp.get_row_stat(0)
|
|
2448
|
+
3
|
|
2449
|
+
"""
|
|
2450
|
+
cdef HighsInt* row_status
|
|
2451
|
+
cdef HighsInt* col_status
|
|
2452
|
+
cdef HighsInt num_rows, num_cols
|
|
2453
|
+
cdef HighsInt status
|
|
2454
|
+
cdef int j
|
|
2455
|
+
|
|
2456
|
+
if i < 0 or i >= self.numrows:
|
|
2457
|
+
raise ValueError("The constraint's index i must satisfy 0 <= i < number_of_constraints")
|
|
2458
|
+
|
|
2459
|
+
if stat < 0 or stat > 4:
|
|
2460
|
+
raise ValueError("Invalid status value. Must be 0-4")
|
|
2461
|
+
|
|
2462
|
+
num_rows = Highs_getNumRow(self.highs)
|
|
2463
|
+
num_cols = Highs_getNumCol(self.highs)
|
|
2464
|
+
|
|
2465
|
+
row_status = <HighsInt*> malloc(num_rows * sizeof(HighsInt))
|
|
2466
|
+
col_status = <HighsInt*> malloc(num_cols * sizeof(HighsInt))
|
|
2467
|
+
|
|
2468
|
+
if row_status == NULL or col_status == NULL:
|
|
2469
|
+
free(row_status)
|
|
2470
|
+
free(col_status)
|
|
2471
|
+
raise MemoryError("Failed to allocate memory")
|
|
2472
|
+
|
|
2473
|
+
try:
|
|
2474
|
+
# Get current basis
|
|
2475
|
+
sig_on()
|
|
2476
|
+
status = Highs_getBasis(self.highs, col_status, row_status)
|
|
2477
|
+
sig_off()
|
|
2478
|
+
|
|
2479
|
+
# Set the new status
|
|
2480
|
+
row_status[i] = stat
|
|
2481
|
+
|
|
2482
|
+
# Set the modified basis
|
|
2483
|
+
sig_on()
|
|
2484
|
+
status = Highs_setBasis(self.highs, col_status, row_status)
|
|
2485
|
+
sig_off()
|
|
2486
|
+
|
|
2487
|
+
if status != kHighsStatusOk:
|
|
2488
|
+
raise MIPSolverException("HiGHS: Failed to set basis")
|
|
2489
|
+
finally:
|
|
2490
|
+
free(row_status)
|
|
2491
|
+
free(col_status)
|
|
2492
|
+
|
|
2493
|
+
cpdef set_col_stat(self, int j, int stat):
|
|
2494
|
+
"""
|
|
2495
|
+
Set the status of a variable.
|
|
2496
|
+
|
|
2497
|
+
INPUT:
|
|
2498
|
+
|
|
2499
|
+
- ``j`` -- the index of the variable
|
|
2500
|
+
|
|
2501
|
+
- ``stat`` -- the status to set to:
|
|
2502
|
+
|
|
2503
|
+
* 0 kLower: non-basic variable at lower bound
|
|
2504
|
+
* 1 kBasic: basic variable
|
|
2505
|
+
* 2 kUpper: non-basic variable at upper bound
|
|
2506
|
+
* 3 kZero: non-basic free variable at zero
|
|
2507
|
+
* 4 kNonbasic: nonbasic (used for unbounded variables)
|
|
2508
|
+
|
|
2509
|
+
.. NOTE::
|
|
2510
|
+
|
|
2511
|
+
HiGHS may reject invalid basis configurations. Setting arbitrary
|
|
2512
|
+
status values may result in the basis being rejected and the
|
|
2513
|
+
original basis being preserved.
|
|
2514
|
+
|
|
2515
|
+
EXAMPLES::
|
|
2516
|
+
|
|
2517
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
2518
|
+
sage: lp = get_solver(solver='HiGHS')
|
|
2519
|
+
sage: lp.add_variables(3)
|
|
2520
|
+
2
|
|
2521
|
+
sage: lp.add_linear_constraint(list(zip([0, 1, 2], [8, 6, 1])), None, 48)
|
|
2522
|
+
sage: lp.add_linear_constraint(list(zip([0, 1, 2], [4, 2, 1.5])), None, 20)
|
|
2523
|
+
sage: lp.add_linear_constraint(list(zip([0, 1, 2], [2, 1.5, 0.5])), None, 8)
|
|
2524
|
+
sage: lp.set_objective([60, 30, 20])
|
|
2525
|
+
sage: lp.solve()
|
|
2526
|
+
0
|
|
2527
|
+
sage: lp.get_col_stat(0)
|
|
2528
|
+
1
|
|
2529
|
+
sage: lp.set_col_stat(0, 2)
|
|
2530
|
+
sage: lp.get_col_stat(0)
|
|
2531
|
+
2
|
|
2532
|
+
"""
|
|
2533
|
+
cdef HighsInt* row_status
|
|
2534
|
+
cdef HighsInt* col_status
|
|
2535
|
+
cdef HighsInt num_rows, num_cols
|
|
2536
|
+
cdef HighsInt status
|
|
2537
|
+
|
|
2538
|
+
if j < 0 or j >= self.numcols:
|
|
2539
|
+
raise ValueError("The variable's index j must satisfy 0 <= j < number_of_variables")
|
|
2540
|
+
|
|
2541
|
+
if stat < 0 or stat > 4:
|
|
2542
|
+
raise ValueError("Invalid status value. Must be 0-4")
|
|
2543
|
+
|
|
2544
|
+
num_rows = Highs_getNumRow(self.highs)
|
|
2545
|
+
num_cols = Highs_getNumCol(self.highs)
|
|
2546
|
+
|
|
2547
|
+
row_status = <HighsInt*> malloc(num_rows * sizeof(HighsInt))
|
|
2548
|
+
col_status = <HighsInt*> malloc(num_cols * sizeof(HighsInt))
|
|
2549
|
+
|
|
2550
|
+
if row_status == NULL or col_status == NULL:
|
|
2551
|
+
free(row_status)
|
|
2552
|
+
free(col_status)
|
|
2553
|
+
raise MemoryError("Failed to allocate memory")
|
|
2554
|
+
|
|
2555
|
+
try:
|
|
2556
|
+
# Get current basis
|
|
2557
|
+
sig_on()
|
|
2558
|
+
status = Highs_getBasis(self.highs, col_status, row_status)
|
|
2559
|
+
sig_off()
|
|
2560
|
+
|
|
2561
|
+
# Set the new status
|
|
2562
|
+
col_status[j] = stat
|
|
2563
|
+
|
|
2564
|
+
# Set the modified basis
|
|
2565
|
+
sig_on()
|
|
2566
|
+
status = Highs_setBasis(self.highs, col_status, row_status)
|
|
2567
|
+
sig_off()
|
|
2568
|
+
|
|
2569
|
+
if status != kHighsStatusOk:
|
|
2570
|
+
raise MIPSolverException("HiGHS: Failed to set basis")
|
|
2571
|
+
finally:
|
|
2572
|
+
free(row_status)
|
|
2573
|
+
free(col_status)
|
|
2574
|
+
|
|
2575
|
+
cpdef int warm_up(self) noexcept:
|
|
2576
|
+
"""
|
|
2577
|
+
Warm up the basis using current statuses assigned to rows and cols.
|
|
2578
|
+
|
|
2579
|
+
This method attempts to validate and use the currently set basis.
|
|
2580
|
+
In HiGHS, setting a basis automatically attempts to factorize it,
|
|
2581
|
+
so this method checks if the current basis is valid.
|
|
2582
|
+
|
|
2583
|
+
OUTPUT: the warming up status
|
|
2584
|
+
|
|
2585
|
+
* 0 The operation has been successfully performed.
|
|
2586
|
+
* -1 The basis is invalid or could not be factorized.
|
|
2587
|
+
|
|
2588
|
+
EXAMPLES::
|
|
2589
|
+
|
|
2590
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
2591
|
+
sage: lp = get_solver(solver = "HiGHS")
|
|
2592
|
+
sage: lp.add_variables(3)
|
|
2593
|
+
2
|
|
2594
|
+
sage: lp.add_linear_constraint(list(zip([0, 1, 2], [8, 6, 1])), None, 48)
|
|
2595
|
+
sage: lp.add_linear_constraint(list(zip([0, 1, 2], [4, 2, 1.5])), None, 20)
|
|
2596
|
+
sage: lp.add_linear_constraint(list(zip([0, 1, 2], [2, 1.5, 0.5])), None, 8)
|
|
2597
|
+
sage: lp.set_objective([60, 30, 20])
|
|
2598
|
+
sage: lp.solve()
|
|
2599
|
+
0
|
|
2600
|
+
sage: lp.get_objective_value()
|
|
2601
|
+
280.0
|
|
2602
|
+
sage: lp.set_row_stat(0, 3)
|
|
2603
|
+
sage: lp.set_col_stat(1, 1)
|
|
2604
|
+
sage: lp.warm_up()
|
|
2605
|
+
0
|
|
2606
|
+
"""
|
|
2607
|
+
cdef HighsInt basis_validity
|
|
2608
|
+
cdef HighsInt status
|
|
2609
|
+
cdef HighsInt* col_status
|
|
2610
|
+
cdef HighsInt* row_status
|
|
2611
|
+
cdef HighsInt num_cols, num_rows
|
|
2612
|
+
|
|
2613
|
+
# Note: HiGHS C API doesn't have getBasisValidity function
|
|
2614
|
+
# Try to get the basis to check if it's available
|
|
2615
|
+
num_cols = Highs_getNumCol(self.highs)
|
|
2616
|
+
num_rows = Highs_getNumRow(self.highs)
|
|
2617
|
+
|
|
2618
|
+
if num_cols == 0 or num_rows == 0:
|
|
2619
|
+
return -1
|
|
2620
|
+
|
|
2621
|
+
col_status = <HighsInt*> malloc(num_cols * sizeof(HighsInt))
|
|
2622
|
+
row_status = <HighsInt*> malloc(num_rows * sizeof(HighsInt))
|
|
2623
|
+
|
|
2624
|
+
if col_status == NULL or row_status == NULL:
|
|
2625
|
+
free(col_status)
|
|
2626
|
+
free(row_status)
|
|
2627
|
+
return -1
|
|
2628
|
+
|
|
2629
|
+
try:
|
|
2630
|
+
status = Highs_getBasis(self.highs, col_status, row_status)
|
|
2631
|
+
if status != kHighsStatusOk:
|
|
2632
|
+
return -1
|
|
2633
|
+
return 0
|
|
2634
|
+
finally:
|
|
2635
|
+
free(col_status)
|
|
2636
|
+
free(row_status)
|
|
2637
|
+
|
|
2638
|
+
cpdef __copy__(self):
|
|
2639
|
+
"""
|
|
2640
|
+
Return a copy of ``self``.
|
|
2641
|
+
|
|
2642
|
+
EXAMPLES::
|
|
2643
|
+
|
|
2644
|
+
sage: from sage.numerical.backends.generic_backend import get_solver
|
|
2645
|
+
sage: p = get_solver(solver='HiGHS')
|
|
2646
|
+
sage: p.add_variables(2)
|
|
2647
|
+
1
|
|
2648
|
+
sage: q = copy(p)
|
|
2649
|
+
sage: q.ncols()
|
|
2650
|
+
2
|
|
2651
|
+
"""
|
|
2652
|
+
cdef HiGHSBackend p
|
|
2653
|
+
cdef HighsInt status
|
|
2654
|
+
cdef bytes temp_file
|
|
2655
|
+
|
|
2656
|
+
import tempfile
|
|
2657
|
+
import os
|
|
2658
|
+
|
|
2659
|
+
p = HiGHSBackend(maximization=self.is_maximization())
|
|
2660
|
+
|
|
2661
|
+
# If model is empty (no constraints), just copy metadata
|
|
2662
|
+
if self.numrows == 0 and self.numcols > 0:
|
|
2663
|
+
# Add the same number of variables
|
|
2664
|
+
for i in range(self.numcols):
|
|
2665
|
+
lb, ub = self.col_bounds(i)
|
|
2666
|
+
p.add_variable(lb, ub,
|
|
2667
|
+
self.objective_coefficient(i),
|
|
2668
|
+
self.is_variable_binary(i),
|
|
2669
|
+
self.is_variable_continuous(i),
|
|
2670
|
+
self.is_variable_integer(i),
|
|
2671
|
+
self.col_name(i))
|
|
2672
|
+
else:
|
|
2673
|
+
# Copy by writing and reading through a temporary MPS file
|
|
2674
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.mps', delete=False) as f:
|
|
2675
|
+
temp_file = f.name.encode('utf-8')
|
|
2676
|
+
|
|
2677
|
+
try:
|
|
2678
|
+
status = Highs_writeModel(self.highs, temp_file)
|
|
2679
|
+
if status != kHighsStatusOk:
|
|
2680
|
+
raise MIPSolverException("HiGHS: Failed to write model for copy")
|
|
2681
|
+
|
|
2682
|
+
status = Highs_readModel(p.highs, temp_file)
|
|
2683
|
+
if status != kHighsStatusOk:
|
|
2684
|
+
raise MIPSolverException("HiGHS: Failed to read model for copy")
|
|
2685
|
+
|
|
2686
|
+
# Turn off logging for the copied model
|
|
2687
|
+
Highs_setBoolOptionValue(p.highs, b"log_to_console", False)
|
|
2688
|
+
finally:
|
|
2689
|
+
if os.path.exists(temp_file.decode('utf-8')):
|
|
2690
|
+
os.unlink(temp_file.decode('utf-8'))
|
|
2691
|
+
|
|
2692
|
+
p.prob_name = self.prob_name
|
|
2693
|
+
p.col_name_var = copy(self.col_name_var)
|
|
2694
|
+
p.row_name_var = copy(self.row_name_var)
|
|
2695
|
+
p.numcols = self.numcols
|
|
2696
|
+
p.numrows = self.numrows
|
|
2697
|
+
p.obj_constant_term = self.obj_constant_term
|
|
2698
|
+
|
|
2699
|
+
return p
|