Skip to content

Commit bbff55c

Browse files
committed
store solution, add var/cons methods
1 parent 89ad2f2 commit bbff55c

File tree

2 files changed

+197
-28
lines changed

2 files changed

+197
-28
lines changed

highs_test.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,12 @@
3232
print(f"Var cols: {solver._var_col}")
3333
print(f"Cons names: {solver._cons_name}")
3434
print(f"Cons cols: {solver._cons_col}")
35+
print(f"Sols: {solver._x}, {solver._rc}, {solver._pi}")
3536

3637
# changes
3738
solver.relax()
3839

40+
# try again
41+
status = model.optimize()
42+
print()
43+
print(f"Sols: {solver._x}, {solver._rc}, {solver._pi}")

mip/highs.py

Lines changed: 192 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,16 @@
140140
HighsInt Highs_setBoolOptionValue(
141141
void* highs, const char* option, const bool value
142142
);
143+
HighsInt Highs_getSolution(
144+
const void* highs, double* col_value, double* col_dual,
145+
double* row_value, double* row_dual
146+
);
147+
HighsInt Highs_deleteRowsBySet(
148+
void* highs, const HighsInt num_set_entries, const HighsInt* set
149+
);
150+
HighsInt Highs_deleteColsBySet(
151+
void* highs, const HighsInt num_set_entries, const HighsInt* set
152+
);
143153
"""
144154

145155
if has_highs:
@@ -173,6 +183,11 @@ def __init__(self, model: mip.Model, name: str, sense: str):
173183
self._cons_col: Dict[str, int] = {}
174184
self._num_int: int = 0
175185

186+
# Also store solution (when available)
187+
self._x = []
188+
self._rc = []
189+
self._pi = []
190+
176191
def __del__(self):
177192
self._lib.Highs_destroy(self._model)
178193

@@ -355,6 +370,24 @@ def optimize(
355370
# TODO: handle relax (need to remember and reset integrality?!
356371
raise NotImplementedError()
357372
status = self._lib.Highs_run(self._model)
373+
374+
# store solution values for later access
375+
if self._has_primal_solution():
376+
# TODO: also handle primal/dual rays?
377+
n, m = self.num_cols(), self.num_rows()
378+
col_value = ffi.new("double[]", n)
379+
col_dual = ffi.new("double[]", n)
380+
row_value = ffi.new("double[]", m)
381+
row_dual = ffi.new("double[]", m)
382+
status = self._lib.Highs_getSolution(
383+
self._model, col_value, col_dual, row_value, row_dual
384+
)
385+
self._x = [col_value[j] for j in range(n)]
386+
self._rc = [col_dual[j] for j in range(n)]
387+
388+
if self._has_dual_solution():
389+
self._pi = [row_dual[i] for i in range(m)]
390+
358391
return self.get_status()
359392

360393
def get_objective_value(self: "SolverHighs") -> numbers.Real:
@@ -506,33 +539,96 @@ def set_verbose(self: "SolverHighs", verbose: int):
506539
# Constraint-related getters/setters
507540

508541
def constr_get_expr(self: "SolverHighs", constr: "mip.Constr") -> "mip.LinExpr":
509-
pass
542+
row = constr.idx
543+
# Call method twice:
544+
# - first, to get the sizes for coefficients,
545+
num_row = ffi.new("int*")
546+
lower = ffi.new("double[]", 1)
547+
upper = ffi.new("double[]", 1)
548+
num_nz = ffi.new("int*")
549+
status = self._lib.Highs_getRowsByRange(
550+
self._model, row, row, lower, upper, num_nz, ffi.NULL, ffi.NULL, ffi.NULL
551+
)
552+
553+
# - second, to get the coefficients in pre-allocated arrays.
554+
matrix_start = ffi.new("int[]", 1)
555+
matrix_index = ffi.new("int[]", num_nz[0])
556+
matrix_value = ffi.new("double[]", num_nz[0])
557+
status = self._lib.Highs_getRowsByRange(
558+
self._model,
559+
row,
560+
row,
561+
lower,
562+
upper,
563+
num_nz,
564+
matrix_start,
565+
matrix_index,
566+
matrix_value,
567+
)
568+
assert matrix[0] == 0
569+
570+
return mip.xsum(matrix_value[i] * self.model.vars[i] for i in range(num_nz))
510571

511572
def constr_set_expr(
512573
self: "SolverHighs", constr: "mip.Constr", value: "mip.LinExpr"
513574
) -> "mip.LinExpr":
514-
pass
575+
raise NotImplementedError()
515576

516577
def constr_get_rhs(self: "SolverHighs", idx: int) -> numbers.Real:
517-
pass
578+
# fetch both lower and upper bound
579+
row = constr.idx
580+
num_row = ffi.new("int*")
581+
lower = ffi.new("double[]", 1)
582+
upper = ffi.new("double[]", 1)
583+
num_nz = ffi.new("int*")
584+
status = self._lib.Highs_getRowsByRange(
585+
self._model, row, row, lower, upper, num_nz, ffi.NULL, ffi.NULL, ffi.NULL
586+
)
587+
588+
# case distinction for sense
589+
if lower[0] == -mip.INF:
590+
return -upper[0]
591+
if upper[0] == mip.INF:
592+
return -lower[0]
593+
assert lower[0] == upper[0]
594+
return -lower[0]
518595

519596
def constr_set_rhs(self: "SolverHighs", idx: int, rhs: numbers.Real):
520-
pass
597+
# first need to figure out which bound to change (lower or upper)
598+
num_row = ffi.new("int*")
599+
lower = ffi.new("double[]", 1)
600+
upper = ffi.new("double[]", 1)
601+
num_nz = ffi.new("int*")
602+
status = self._lib.Highs_getRowsByRange(
603+
self._model, idx, idx, lower, upper, num_nz, ffi.NULL, ffi.NULL, ffi.NULL
604+
)
605+
606+
# update bounds as needed
607+
lb, ub = lower[0], upper[0]
608+
if lb != -mip.INF:
609+
lb = -rhs
610+
if ub != mip.INF:
611+
ub = -rhs
612+
613+
# set new bounds
614+
status = self._lib.Highs_changeRowBounds(self._model, idx, lb, ub)
521615

522616
def constr_get_name(self: "SolverHighs", idx: int) -> str:
523-
pass
617+
return self._cons_name(idx)
524618

525619
def constr_get_pi(self: "SolverHighs", constr: "mip.Constr") -> numbers.Real:
526-
pass
620+
if self._pi:
621+
return self._pi[constr.idx]
527622

528623
def constr_get_slack(self: "SolverHighs", constr: "mip.Constr") -> numbers.Real:
529-
pass
624+
raise NotImplementedError()
530625

531626
def remove_constrs(self: "SolverHighs", constrsList: List[int]):
532-
pass
627+
set_ = ffi.new("int[]", constrsList)
628+
status = self._lib.Highs_deleteRowsBySet(self._model, len(constrsList), set_)
533629

534630
def constr_get_index(self: "SolverHighs", name: str) -> int:
535-
pass
631+
return self._cons_col(name)
536632

537633
# Variable-related getters/setters
538634

@@ -545,49 +641,115 @@ def var_set_branch_priority(
545641
raise NotImplementedError()
546642

547643
def var_get_lb(self: "SolverHighs", var: "mip.Var") -> numbers.Real:
548-
pass
644+
num_col = ffi.new("int*")
645+
costs = ffi.new("double[]", 1)
646+
lower = ffi.new("double[]", 1)
647+
upper = ffi.new("double[]", 1)
648+
num_nz = ffi.new("int*")
649+
status = self._lib.Highs_getColsByRange(
650+
self._model,
651+
var.idx, # from_col
652+
var.idx, # to_col
653+
num_col,
654+
costs,
655+
lower,
656+
upper,
657+
num_nz,
658+
ffi.NULL, # matrix_start
659+
ffi.NULL, # matrix_index
660+
ffi.NULL, # matrix_value
661+
)
662+
return lower[0]
549663

550664
def var_set_lb(self: "SolverHighs", var: "mip.Var", value: numbers.Real):
551-
pass
665+
# can only set both bounds, so we just set the old upper bound
666+
old_upper = self.var_get_ub(var)
667+
status = self._lib.Highs_changeColBounds(self._model, var.idx, value, old_upper)
552668

553669
def var_get_ub(self: "SolverHighs", var: "mip.Var") -> numbers.Real:
554-
pass
670+
num_col = ffi.new("int*")
671+
costs = ffi.new("double[]", 1)
672+
lower = ffi.new("double[]", 1)
673+
upper = ffi.new("double[]", 1)
674+
num_nz = ffi.new("int*")
675+
status = self._lib.Highs_getColsByRange(
676+
self._model,
677+
var.idx, # from_col
678+
var.idx, # to_col
679+
num_col,
680+
costs,
681+
lower,
682+
upper,
683+
num_nz,
684+
ffi.NULL, # matrix_start
685+
ffi.NULL, # matrix_index
686+
ffi.NULL, # matrix_value
687+
)
688+
return upper[0]
555689

556690
def var_set_ub(self: "SolverHighs", var: "mip.Var", value: numbers.Real):
557-
pass
691+
# can only set both bounds, so we just set the old lower bound
692+
old_lower = self.var_get_lb(var)
693+
status = self._lib.Highs_changeColBounds(self._model, var.idx, old_lower, value)
558694

559695
def var_get_obj(self: "SolverHighs", var: "mip.Var") -> numbers.Real:
560-
pass
696+
num_col = ffi.new("int*")
697+
costs = ffi.new("double[]", 1)
698+
lower = ffi.new("double[]", 1)
699+
upper = ffi.new("double[]", 1)
700+
num_nz = ffi.new("int*")
701+
status = self._lib.Highs_getColsByRange(
702+
self._model,
703+
var.idx, # from_col
704+
var.idx, # to_col
705+
num_col,
706+
costs,
707+
lower,
708+
upper,
709+
num_nz,
710+
ffi.NULL, # matrix_start
711+
ffi.NULL, # matrix_index
712+
ffi.NULL, # matrix_value
713+
)
714+
return costs[0]
561715

562716
def var_set_obj(self: "SolverHighs", var: "mip.Var", value: numbers.Real):
563-
pass
717+
status = self._lib.Highs_changeColCost(self._model, var.idx, value)
564718

565719
def var_get_var_type(self: "SolverHighs", var: "mip.Var") -> str:
566-
pass
720+
# TODO: store var type separately?
721+
raise NotImplementedError()
567722

568723
def var_set_var_type(self: "SolverHighs", var: "mip.Var", value: str):
569-
pass
724+
# TODO: store var type separately?
725+
raise NotImplementedError()
570726

571727
def var_get_column(self: "SolverHighs", var: "mip.Var") -> "Column":
572-
pass
728+
# TODO
729+
raise NotImplementedError()
573730

574731
def var_set_column(self: "SolverHighs", var: "mip.Var", value: "Column"):
575-
pass
732+
# TODO
733+
raise NotImplementedError()
576734

577735
def var_get_rc(self: "SolverHighs", var: "mip.Var") -> numbers.Real:
578-
pass
736+
# TODO: double-check this!
737+
if self._rc:
738+
self._rc[var.idx]
579739

580740
def var_get_x(self: "SolverHighs", var: "mip.Var") -> numbers.Real:
581-
pass
741+
if self._x:
742+
return self._x[var.idx]
582743

583744
def var_get_xi(self: "SolverHighs", var: "mip.Var", i: int) -> numbers.Real:
584-
pass
745+
raise NotImplementedError()
585746

586747
def var_get_name(self: "SolverHighs", idx: int) -> str:
587748
return self._var_name[idx]
588749

589750
def remove_vars(self: "SolverHighs", varsList: List[int]):
590-
pass
751+
set_ = ffi.new("int[]", varsList)
752+
status = self._lib.Highs_deleteColsBySet(self._model, len(varsList), set_)
591753

592754
def var_get_index(self: "SolverHighs", name: str) -> int:
593755
return self._var_col[name]
@@ -599,17 +761,19 @@ def set_problem_name(self: "SolverHighs", name: str):
599761
self._name = name
600762

601763
def _get_primal_solution_status(self: "SolverHighs"):
602-
sol_status = ffi.new("int*")
603-
status = self._lib.Highs_getIntInfoValue(
604-
self._model, "primal_solution_status", sol_status
605-
)
606-
return sol_status[0]
764+
return self._get_int_info_value("primal_solution_status")
607765

608766
def _has_primal_solution(self: "SolverHighs"):
609767
return (
610768
self._get_primal_solution_status() == self._lib.kHighsSolutionStatusFeasible
611769
)
612770

771+
def _get_dual_solution_status(self: "SolverHighs"):
772+
return self._get_int_info_value("dual_solution_status")
773+
774+
def _has_dual_solution(self: "SolverHighs"):
775+
return self._get_dual_solution_status() == self._lib.kHighsSolutionStatusFeasible
776+
613777
def get_status(self: "SolverHighs") -> mip.OptimizationStatus:
614778
OS = mip.OptimizationStatus
615779
status_map = {

0 commit comments

Comments
 (0)