passagemath-highs 10.8.1rc3__cp314-cp314-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.
@@ -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