Skip to content

Support iterative solve of the same solver_model (when using highs) #198

@dannyopts

Description

@dannyopts

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions