Skip to content

Commit

Permalink
Extract set_warm_start stuff from init_model in rcpsp milp solvers
Browse files Browse the repository at this point in the history
We can avoid to call init_model() in set_warm_start() soing so.
For now we still keep the same behaviour of init_model though, by
calling set_warm_start at the end of init_model to initialize the lp
solver (mathopt or gurobi).
  • Loading branch information
nhuet committed Sep 30, 2024
1 parent 8a3f09f commit 5d48622
Show file tree
Hide file tree
Showing 2 changed files with 57 additions and 62 deletions.
113 changes: 54 additions & 59 deletions discrete_optimization/rcpsp/solver/rcpsp_lp_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ def __init__(
def init_model(self, **kwargs):
greedy_start = kwargs.get("greedy_start", True)
start_solution = kwargs.get("start_solution", None)
self.hinted_values = {}
if start_solution is None:
if greedy_start:
logger.info("Computing greedy solution")
Expand Down Expand Up @@ -177,17 +176,6 @@ def init_model(self, **kwargs):
)
>= p[self.index_in_var[j]]
)
for j in self.index_task:
for t in self.index_time:
if (
self.start_solution.rcpsp_schedule[self.problem.tasks_list[j]][
"start_time"
]
== t
):
self.hinted_values[self.x[j][t]] = 1
else:
self.hinted_values[self.x[j][t]] = 0
p_s: Optional[PartialSolution] = kwargs.get("partial_solution", None)
self.partial_solution = p_s
self.constraints_partial_solutions = []
Expand Down Expand Up @@ -291,14 +279,27 @@ def init_model(self, **kwargs):
]
self.constraints_partial_solutions = constraints
# take into account "warmstart" w/o calling set_warmstart (would cause a recursion issue here)
self.set_warm_start_from_values(variable_values=self.hinted_values)
self.set_warm_start(self.start_solution)

def set_warm_start_from_values(
self,
variable_values: dict[VariableType, float],
dual_values: Optional[dict[ConstraintType, float]] = None,
) -> None:
"""
def convert_to_variable_values(
self, solution: RCPSPSolution
) -> dict[VariableType, float]:
hinted_values: dict[VariableType, float] = {}
for j in self.index_task:
for t in self.index_time:
if (
self.start_solution.rcpsp_schedule[self.problem.tasks_list[j]][
"start_time"
]
== t
):
hinted_values[self.x[j][t]] = 1
else:
hinted_values[self.x[j][t]] = 0
return hinted_values

def set_warm_start(self, solution: Solution) -> None:
"""Make the solver warm start from the given solution.
Implemented by OrtoolsMathOptMilpSolver or GurobiMilpSolver.
Expand Down Expand Up @@ -537,16 +538,11 @@ def init_model(self, **args):
class LP_RCPSP_MATHOPT(OrtoolsMathOptMilpSolver, _BaseLP_RCPSP):
hyperparameters = _BaseLP_RCPSP.hyperparameters
problem: RCPSPModel
hinted_values: dict[mathopt.Variable, float]

def convert_to_variable_values(
self, solution: RCPSPSolution
) -> dict[mathopt.Variable, float]:
self.init_model(
partial_solution=self.partial_solution,
start_solution=solution,
)
return self.hinted_values
return _BaseLP_RCPSP.convert_to_variable_values(self, solution=solution)


class _BaseLP_MRCPSP(MilpSolver, SolverRCPSP):
Expand All @@ -560,7 +556,6 @@ class _BaseLP_MRCPSP(MilpSolver, SolverRCPSP):
x: dict[tuple[Hashable, int, int], VariableType]
max_horizon: Optional[int] = None
partial_solution: Optional[PartialSolution] = None
hinted_values: dict[VariableType, float]

def __init__(
self,
Expand Down Expand Up @@ -724,42 +719,19 @@ def init_model(self, **args):
>= durations[j]
)

hinted_values_tuple_list = []
self.starts = {}
for task in sorted_tasks:
self.starts[task] = self.add_integer_variable_to_internal_model(
name=f"start({task})",
lb=0,
ub=self.index_time[-1],
)
if task in self.start_solution.rcpsp_schedule:
hinted_values_tuple_list.append(
(
self.starts[task],
self.start_solution.rcpsp_schedule[task]["start_time"],
)
)
self.add_linear_constraint_to_internal_model(
self.construct_linear_sum_for_internal_model(
[self.x[key] * key[2] for key in variable_per_task[task]]
)
== self.starts[task]
)
modes_dict = self.problem.build_mode_dict(self.start_solution.rcpsp_modes)
for j in self.start_solution.rcpsp_schedule:
start_time_j = self.start_solution.rcpsp_schedule[j]["start_time"]
hinted_values_tuple_list.append(
(
self.durations[j],
self.problem.mode_details[j][modes_dict[j]]["duration"],
)
)
for k in self.variable_per_task[j]:
task, mode, time = k
if start_time_j == time and mode == modes_dict[j]:
hinted_values_tuple_list.append((self.x[k], 1))
else:
hinted_values_tuple_list.append((self.x[k], 0))

p_s: Optional[PartialSolution] = args.get("partial_solution", None)
self.partial_solution = p_s
Expand Down Expand Up @@ -849,8 +821,15 @@ def init_model(self, **args):
self.update_model()

# take into account "warmstart" w/o calling set_warmstart (would cause a recursion issue here)
self.hinted_values = dict(hinted_values_tuple_list)
self.set_warm_start_from_values(variable_values=self.hinted_values)
self.set_warm_start(self.start_solution)

def set_warm_start(self, solution: Solution) -> None:
"""Make the solver warm start from the given solution.
Implemented by OrtoolsMathOptMilpSolver or GurobiMilpSolver.
"""
raise NotImplementedError()

def update_model(self) -> None:
"""Update model (especially for gurobi).
Expand Down Expand Up @@ -884,17 +863,34 @@ def retrieve_current_solution(
rcpsp_schedule_feasible=True,
)

def convert_to_variable_values(self, solution: RCPSPSolution) -> dict[Any, float]:
def convert_to_variable_values(
self, solution: RCPSPSolution
) -> dict[VariableType, float]:
"""Convert a solution to a mapping between model variables and their values.
Will be used by set_warm_start().
"""
self.init_model(
max_horizon=self.max_horizon,
partial_solution=self.partial_solution,
start_solution=solution,
)
return self.hinted_values
hinted_values: dict[VariableType, float] = {}
for task in self.problem.tasks_list:
if task in solution.rcpsp_schedule:
hinted_values[self.starts[task]] = solution.rcpsp_schedule[task][
"start_time"
]
modes_dict = self.problem.build_mode_dict(solution.rcpsp_modes)
for j in solution.rcpsp_schedule:
start_time_j = solution.rcpsp_schedule[j]["start_time"]
hinted_values[self.durations[j]] = self.problem.mode_details[j][
modes_dict[j]
]["duration"]
for k in self.variable_per_task[j]:
task, mode, time = k
if start_time_j == time and mode == modes_dict[j]:
hinted_values[self.x[k]] = 1
else:
hinted_values[self.x[k]] = 0

return hinted_values


class LP_MRCPSP(PymipMilpSolver, _BaseLP_MRCPSP):
Expand Down Expand Up @@ -1163,7 +1159,6 @@ class LP_MRCPSP_MATHOPT(OrtoolsMathOptMilpSolver, _BaseLP_MRCPSP):

max_horizon: Optional[int] = None
partial_solution: Optional[PartialSolution] = None
hinted_values: dict[mathopt.Variable, float]

def convert_to_variable_values(
self, solution: RCPSPSolution
Expand Down
6 changes: 3 additions & 3 deletions tests/rcpsp/solver/test_rcpsp_lp.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def test_rcpsp_sm_lp_mathopt():
files_available = get_data_available()
file = [f for f in files_available if "j301_1.sm" in f][0]
rcpsp_problem = parse_file(file)
solver = LP_RCPSP_MATHOPT(problem=rcpsp_problem, lp_solver=MilpSolverName.CBC)
solver = LP_RCPSP_MATHOPT(problem=rcpsp_problem)
solver.init_model()
results_storage: ResultStorage = solver.solve(
parameters_milp=ParametersMilp.default()
Expand All @@ -122,7 +122,7 @@ def test_rcpsp_mm_lp_mathopt():
file = [f for f in files_available if "j1010_1.mm" in f][0]
rcpsp_problem = parse_file(file)
rcpsp_problem.set_fixed_modes([1 for i in range(rcpsp_problem.n_jobs)])
solver = LP_MRCPSP_MATHOPT(problem=rcpsp_problem, lp_solver=MilpSolverName.CBC)
solver = LP_MRCPSP_MATHOPT(problem=rcpsp_problem)
solver.init_model(greedy_start=False)
results_storage: ResultStorage = solver.solve(
parameters_milp=ParametersMilp.default()
Expand Down Expand Up @@ -150,7 +150,7 @@ def test_rcpsp_sm_lp_mathopt_partial():
}
partial_solution = PartialSolution(task_mode=None, start_times=some_constraints)
partial_solution_for_lp = partial_solution
solver = LP_RCPSP_MATHOPT(problem=rcpsp_problem, lp_solver=MilpSolverName.CBC)
solver = LP_RCPSP_MATHOPT(problem=rcpsp_problem)
solver.init_model(partial_solution=partial_solution_for_lp, greedy_start=False)
store = solver.solve(time_limit=20)
solution, fit = store.get_best_solution_fit()
Expand Down

0 comments on commit 5d48622

Please sign in to comment.