linopy icon indicating copy to clipboard operation
linopy copied to clipboard

Properly free resources after solve with gurobipy

Open FBumann opened this issue 7 months ago • 6 comments

Version Checks (indicate both or one)

  • [x] I have confirmed this bug exists on the lastest release of Linopy.

  • [x] I have confirmed this bug exists on the current master branch of Linopy.

Issue Description

Using gurobipy with a non-academic license leads to issues with the license not being freed after the model was solved. I would

Im not finished investigating the issue, but it probably arises from not closing the gp.Model before closing the gp.Env The gurobipy website says the following:

It is good practice to use the with keyword when dealing with environment (and model) objects. That way the resources tied to these objects are properly released even if an exception is raised at some point. The following example illustrates two typical use patterns.

import gurobipy as gp
with gp.Env("gurobi.log") as env, gp.Model(env=env) as model:
    # Populate model object here...
    model.optimize()

with gp.Env(empty=True) as env:
    env.setParam("ComputeServer", "myserver1:32123")
    env.setParam("ServerPassword", "pass")
    env.start()
    with gp.Model(env=env) as model:
        # Populate model object here...
        model.optimize()

I will correct this and open a PR. Is there any reason to not close the model after the solve? Maybe because of IIS calculation?

Reproducible Example

Unfortunately I can provide a reproducible example without a usage restricted license

Expected Behavior

I would expect to be able to solve a linty model in a new python thread/console, after the model in another console finished. This is not possible, due to the license not being released properly.

Installed Versions

bottleneck==1.5.0 click==8.2.0 cloudpickle==3.1.1 dask==2025.5.0 deprecation==2.1.0 fsspec==2025.3.2 importlib-metadata==8.7.0 linopy==0.5.5 locket==1.0.0 numexpr==2.10.2 numpy==2.2.6 packaging==25.0 pandas==2.2.3 partd==1.4.2 polars==1.29.0 python-dateutil==2.9.0.post0 pytz==2025.2 pyyaml==6.0.2 scipy==1.15.3 six==1.17.0 toolz==1.0.0 tqdm==4.67.1 tzdata==2025.2 xarray==2025.4.0 zipp==3.21.0

FBumann avatar May 20 '25 14:05 FBumann

Hey @FBumann thanks for the issue. Will closing the model afterwards not allow to compute the IIS?

FabianHofmann avatar Jun 05 '25 09:06 FabianHofmann

Probably. Maybe an option in solve() would be the best option to navigate around this. It took a bit of digging to realize why I'm having the issue described above. I think this will be a common problem in a non-academic environment, so a quick fix would be nice I think.

FBumann avatar Jun 05 '25 17:06 FBumann

This code snippet is what I use to perform the cleaning up recommended in the gurobi docs:

@contextlib.contextmanager
def lp_env_manager(lp_model: linopy.Model, solver: SolverName) -> Generator[gurobipy.Env | None]:
    """Context manager for handling a Gurobi solver environment with a linopy Model.

    Intended to be wrapped around calls to the solve() method of an existing linopy Model. Using
    this ensures that the Gurobi license environment is started and disposed of cleanly.

    >>> solver_name = "gurobi"
    >>> with lp_env_manager(model, solver_name) as env:
    >>>    model.solve(solver_name=solver_name, env=env)
    """
    env = None
    if solver == "gurobi":
        env = gurobipy.Env(
            empty=True,
            params={
                "LogToConsole": 0,
                **GurobiLic().model_dump(),  # pyright: ignore[reportCallIssue]
            },
        )

    try:
        if isinstance(env, gurobipy.Env):
            env.start()
        yield env
    except Exception as e:
        if isinstance(e, gurobipy.GurobiError) and e.errno == gurobipy.GRB.ERROR_NO_LICENSE:
            raise SolverLicenseError(e.message) from e
        raise
    finally:
        if isinstance(m := getattr(lp_model, "solver_model", None), gurobipy.Model):
            m.dispose()
            logger.info("Gurobi model disposed.")
        if isinstance(env, gurobipy.Env):
            env.dispose()
            gurobipy.disposeDefaultEnv()
            logger.info("Gurobi environment disposed.")

ollie-bell avatar Jul 01 '25 08:07 ollie-bell

I think the gurobipy.Model needs to be treated in the same way as the gurobipy.Env. i.e. it needs to be wrapped in stack.enter_context and/or allow a user to instantiate their own gurobipy.Model instance outside of linopy (as they can with gurobipy.Env).

In the former case, .compute_infeasibilities() method will not work after a .solve() since the gurobipy.Model will have already been cleaned up. You could add an optional argument to the .solve() method in which the user decides before the solve whether they want to computeIIS in the event the model is infeasible.

In the latter case, the gurobipy.Model would still be alive as the user is responsible for managing the object, therefore .compute_infeasibilities() would still work and any time before the user decides to dispose of the model.

ollie-bell avatar Jul 28 '25 13:07 ollie-bell

@ollie-bell After closing the model, .compute_infeasibilities()will not work anymore. In production environments, this is not needed most of the time. But closing the Model will free up the license. One can of course close the Model manually after the solve (that's what im doing for now), but I think a parameter to close the Model (and Environment) after solving in the .solve()method would be very handy for many people. I will post my currently used code tomorrow

FBumann avatar Jul 29 '25 17:07 FBumann

@ollie-bell @FabianHofmann Im currently simply closing the model after the solve

import linopy
m = linopy.Model()
m.add_variables()
m.solve('gurobi')

m.solver_model.close()

print(m.solver_model)

Maybe it's enough to put this in the docstring of the .solve() method, as it's only relevant for commercial solvers with commercial licenses. But it was quite annoying for me to figure out this simple fix.

FBumann avatar Jul 30 '25 07:07 FBumann