44from numbers import Integral
55from itertools import product
66from threading import Thread , local , RLock , Lock
7+ from multiprocessing import cpu_count
78from typing import Optional , Any , overload , Callable , Sequence , Mapping , Iterable , SupportsIndex , cast , Union
89
910from ._core import (
@@ -29,17 +30,19 @@ class Highs(_Highs):
2930 HiGHS solver interface
3031 """
3132
32- __handle_keyboard_interrupt : bool = False
33- __handle_user_interrupt : bool = False
34- __solver_should_stop : bool = False
35- __solver_stopped : RLock = RLock ()
36- __solver_started : Lock = Lock ()
37- __solver_status : Optional [HighsStatus ] = None
38-
3933 def __init__ (self ):
4034 super ().__init__ ()
4135 self .callbacks = [HighsCallback (cb .HighsCallbackType (_ ), self ) for _ in range (int (cb .HighsCallbackType .kCallbackMax ) + 1 )]
4236 self .enableCallbacks ()
37+
38+ self .__handle_keyboard_interrupt : bool = False
39+ self .__handle_user_interrupt : bool = False
40+ self .__use_concurrent_solve : bool = False
41+
42+ self .__solver_should_stop : bool = False
43+ self .__solver_stopped : RLock = RLock ()
44+ self .__solver_started : Lock = Lock ()
45+ self .__solver_status : Optional [HighsStatus ] = None
4346
4447 # Silence logging
4548 def silent (self , turn_off_output : bool = True ):
@@ -55,10 +58,70 @@ def solve(self):
5558 Returns:
5659 A HighsStatus object containing the solve status.
5760 """
58- if not self .HandleKeyboardInterrupt :
59- return super ().run ()
61+ if not self .ConcurrentSolve :
62+ if not self .HandleKeyboardInterrupt :
63+ return super ().run ()
64+ else :
65+ return self .joinSolve (self .startSolve ())
6066 else :
61- return self .joinSolve (self .startSolve ())
67+ num_threads = self .getOptionValue ("threads" )
68+ if num_threads == 0 :
69+ num_threads = int (cpu_count () / 2 )
70+
71+ clones = [self ] + [Highs () for _ in range (num_threads - 1 )]
72+
73+ __best_solution = Lock ()
74+ __best = [None , np .zeros (self .getNumCol ())] # objective, solution
75+
76+ if self .getObjectiveSense ()[1 ] == ObjSense .kMinimize :
77+ is_better = lambda a ,b : a < b
78+ current_objective = [self .inf ] * num_threads
79+ else :
80+ is_better = lambda a ,b : a > b
81+ current_objective = [- self .inf ] * num_threads
82+
83+ def get_solution (e ):
84+ current_objective [int (e .user_data )] = e .data_out .objective_function_value
85+
86+ with __best_solution :
87+ if __best [0 ] == None or is_better (e .data_out .objective_function_value , __best [0 ]):
88+ __best [0 ] = e .data_out .objective_function_value
89+ __best [1 ][:] = e .data_out .mip_solution
90+ # print("Better incumbent found", int(e.user_data), __best[0])
91+
92+ def put_solution (e ):
93+ with __best_solution :
94+ if __best [0 ] != None and is_better (__best [0 ], current_objective [int (e .user_data )]):
95+ # print("Updating thread", int(e.user_data), __best[0], current_objective[int(e.user_data)])
96+ e .data_in .user_has_solution = True
97+ e .data_in .user_solution [:] = __best [1 ]
98+ current_objective [int (e .user_data )] = __best [0 ]
99+
100+ clones [0 ].cbMipImprovingSolution .subscribe (get_solution , 0 )
101+ clones [0 ].cbMipUserSolution .subscribe (put_solution , 0 )
102+ clones [0 ].HandleUserInterrupt = True
103+
104+ for i in range (1 , num_threads ):
105+ clones [i ].silent ()
106+ clones [i ].setOptionValue ("random_seed" , i )
107+ clones [i ].HandleUserInterrupt = True
108+ clones [i ].passModel (self .getModel ())
109+ clones [i ].cbMipImprovingSolution .subscribe (get_solution , i )
110+ clones [i ].cbMipUserSolution .subscribe (put_solution , i )
111+
112+ threads = []
113+
114+ for i in range (num_threads ):
115+ threads .append (clones [i ].startSolve ())
116+
117+ clones [0 ].joinSolve (threads [0 ])
118+ clones [0 ].cbMipImprovingSolution .unsubscribe (get_solution )
119+ clones [0 ].cbMipUserSolution .unsubscribe (put_solution )
120+
121+ for i in range (1 , num_threads ):
122+ clones [i ].cancelSolve ()
123+ clones [i ].joinSolve (threads [i ])
124+
62125
63126 def startSolve (self ):
64127 """
@@ -1271,6 +1334,17 @@ def __user_interrupt_event(self, e: HighsCallbackEvent):
12711334 if self .__solver_should_stop :
12721335 e .interrupt ()
12731336
1337+ @property
1338+ def ConcurrentSolve (self ):
1339+ """
1340+ Get/Set whether the solver should run on separate threads with different random seeds
1341+ """
1342+ return self .__use_concurrent_solve
1343+
1344+ @ConcurrentSolve .setter
1345+ def ConcurrentSolve (self , value : bool ):
1346+ self .__use_concurrent_solve = value
1347+
12741348 @property
12751349 def cbLogging (self ):
12761350 return self .callbacks [int (cb .HighsCallbackType .kCallbackLogging )]
0 commit comments