-
Notifications
You must be signed in to change notification settings - Fork 74
Description
I'm not sure what the right approach is here, so I'm going to write what I have done and found.
I want to be able to solve the same model repeatedly with an altered cost function to understand model sensitivity.
My specific use case is that I have a PyPSA model which takes about 50 seconds to solve using highs.
I want to solve this model many time with different costs to understand If I update the cost function.
One way is in a loop, just keep resolving the problem, but I want to take advantage of the warm start power of highs.
I have done this by accessing the solver_model and calling
m.solver_model.changeColsCost(len(coeffsi), np.arange(len(coeffsi), dtype=np.int32), coeffsi)
See below for 15 times taken for this kind of sequential solve, this gives approximately a 5x speed up vs the independent sequential solve (the first is kind of representative of the time for solving the problem without warm start)
[58.32, 23.88, 2.74 8.54, 14.34, 17.98, 2.55, 22.21, 20.98, 24.55, 17.32, 13.22, 5.00, 4.12, 3.07]
But then I cant apply this solution back to the linopy model without extracting a bunch of code from model.solve related to applying the solution from a solver_model to the model.
def apply_solution(model, h):
CONDITION_MAP = {}
condition = h.modelStatusToString(h.getModelStatus()).lower()
termination_condition = CONDITION_MAP.get(condition, condition)
status = Status.from_termination_condition(termination_condition)
status.legacy_status = condition
def get_solver_solution() -> Solution:
objective = h.getObjectiveValue()
solution = h.getSolution()
sol = pd.Series(solution.col_value, h.getLp().col_names_, dtype=float).pipe(
set_int_index
)
dual = pd.Series(solution.row_dual, h.getLp().row_names_, dtype=float).pipe(
set_int_index
)
return Solution(sol, dual, objective)
solution = safe_get_solution(status, get_solver_solution)
result = Result(status, solution, h)
model.objective._value = result.solution.objective
model.status = result.status.status.value
model.termination_condition = result.status.termination_condition.value
model.solver_model = result.solver_model
sol = result.solution.primal.copy()
sol.loc[-1] = np.nan
for name, var in model.variables.items():
idx = np.ravel(var.labels)
try:
vals = sol[idx].values.reshape(var.labels.shape)
except KeyError:
vals = sol.reindex(idx).values.reshape(var.labels.shape)
var.solution = xr.DataArray(vals, var.coords)
By pulling out the application of a solution from the solve method as a seperate public function, it would make this usage much easier.
I know that the run_{solver} methods take in a warmstart_fn, but this isnt supported for highs, maybe simply supporting this would be sufficent? But having experimented with this I think that the last release of highspy doesnt yet have the bindings for setSolution, but master does (presumably this is why it isnt supported?)
This would incur an additional overhead of writing and reading the basis file on each loop, but maybe that is fine for most usecases (I havent been able to test how this impacts performance, since I dont have v1.6 of highs).
Am I doing something stupid, or is this kind of iterative solve not supported with highs via linopy right now? Would it be something that you want to support?
If yes, I would be happy to work on a PR if you could help me scope out what the right interfaces are.
For a bit more context here is an example of how I am using this code with pypsa:
def solve_range(start_price, step_size, iterations):
price = start_price
n = create_network(price)
# solve the initial model
n.optimize(solver_name="highs", parallel="on")
m = n.model
del n.model
yield n, time.time() - start
for i in range(iterations):
price += step_size
# create the similar but different network (with n.model already created)
n_i = create_model(price)
mi = n_i._network.model
dfi = mi.objective.flat
coeffsi = dfi.coeffs.values
# update the highs model to include the new cost col - the constraints dont vary between runs
m.solver_model.changeColsCost(len(coeffsi), np.arange(len(coeffsi), dtype=np.int32), coeffsi)
# solve the highs model
m.solver_model.run()
# write the solution back to the model
apply_solution(m, m.solver_model)
n_i.model = m
# write the solution back to the network (functions imported from pypsa)
assign_solution(n_i)
assign_duals(n_i)
post_processing(n_i)
del n_i.model
yield h2_grid_model_i, end - start