Skip to content

Commit d5c5b1e

Browse files
authored
feat: allow passing a parameter dict to gurobi's env creation (#469)
* feat: allow passing a parameter dict to gurobi's env creation * improve doc and type annotations * Add type variable for solver's environment type * Fix code coverage * Import model fixture from test_io
1 parent 7e8f363 commit d5c5b1e

File tree

4 files changed

+89
-23
lines changed

4 files changed

+89
-23
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
.coverage
22
.eggs
33
.DS_Store
4+
.mypy_cache
45
linopy/__pycache__
56
test/__pycache__
67
linopy.egg-info

doc/release_notes.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Release Notes
77
88
99
* Improved variable/expression arithmetic methods so that they correctly handle types
10+
* Gurobi: Pass dictionary as env argument `env={...}` through to gurobi env creation
1011

1112
**Breaking Changes**
1213

linopy/solvers.py

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from collections import namedtuple
1818
from collections.abc import Callable, Generator
1919
from pathlib import Path
20-
from typing import TYPE_CHECKING, Any
20+
from typing import TYPE_CHECKING, Any, Generic, TypeVar
2121

2222
import numpy as np
2323
import pandas as pd
@@ -32,8 +32,12 @@
3232
)
3333

3434
if TYPE_CHECKING:
35+
import gurobipy
36+
3537
from linopy.model import Model
3638

39+
EnvType = TypeVar("EnvType")
40+
3741
QUADRATIC_SOLVERS = [
3842
"gurobi",
3943
"xpress",
@@ -195,7 +199,7 @@ def maybe_adjust_objective_sign(
195199
return solution
196200

197201

198-
class Solver(ABC):
202+
class Solver(ABC, Generic[EnvType]):
199203
"""
200204
Abstract base class for solving a given linear problem.
201205
@@ -242,7 +246,7 @@ def solve_problem_from_model(
242246
log_fn: Path | None = None,
243247
warmstart_fn: Path | None = None,
244248
basis_fn: Path | None = None,
245-
env: None = None,
249+
env: EnvType | None = None,
246250
explicit_coordinate_names: bool = False,
247251
) -> Result:
248252
"""
@@ -262,7 +266,7 @@ def solve_problem_from_file(
262266
log_fn: Path | None = None,
263267
warmstart_fn: Path | None = None,
264268
basis_fn: Path | None = None,
265-
env: None = None,
269+
env: EnvType | None = None,
266270
) -> Result:
267271
"""
268272
Abstract method to solve a linear problem from a problem file.
@@ -281,7 +285,7 @@ def solve_problem(
281285
log_fn: Path | None = None,
282286
warmstart_fn: Path | None = None,
283287
basis_fn: Path | None = None,
284-
env: None = None,
288+
env: EnvType | None = None,
285289
explicit_coordinate_names: bool = False,
286290
) -> Result:
287291
"""
@@ -322,7 +326,7 @@ def solver_name(self) -> SolverName:
322326
return SolverName[self.__class__.__name__]
323327

324328

325-
class CBC(Solver):
329+
class CBC(Solver[None]):
326330
"""
327331
Solver subclass for the CBC solver.
328332
@@ -503,7 +507,7 @@ def get_solver_solution() -> Solution:
503507
return Result(status, solution, CbcModel(mip_gap, runtime))
504508

505509

506-
class GLPK(Solver):
510+
class GLPK(Solver[None]):
507511
"""
508512
Solver subclass for the GLPK solver.
509513
@@ -673,7 +677,7 @@ def get_solver_solution() -> Solution:
673677
return Result(status, solution)
674678

675679

676-
class Highs(Solver):
680+
class Highs(Solver[None]):
677681
"""
678682
Solver subclass for the Highs solver. Highs must be installed
679683
for usage. Find the documentation at https://www.maths.ed.ac.uk/hall/HiGHS/.
@@ -919,7 +923,7 @@ def get_solver_solution() -> Solution:
919923
return Result(status, solution, h)
920924

921925

922-
class Gurobi(Solver):
926+
class Gurobi(Solver[gurobipy.Env | dict[str, Any] | None]):
923927
"""
924928
Solver subclass for the gurobi solver.
925929
@@ -942,7 +946,7 @@ def solve_problem_from_model(
942946
log_fn: Path | None = None,
943947
warmstart_fn: Path | None = None,
944948
basis_fn: Path | None = None,
945-
env: None = None,
949+
env: gurobipy.Env | dict[str, Any] | None = None,
946950
explicit_coordinate_names: bool = False,
947951
) -> Result:
948952
"""
@@ -962,8 +966,8 @@ def solve_problem_from_model(
962966
Path to the warmstart file.
963967
basis_fn : Path, optional
964968
Path to the basis file.
965-
env : None, optional
966-
Gurobi environment for the solver
969+
env : gurobipy.Env or dict, optional
970+
Gurobi environment for the solver, pass env directly or kwargs for creation.
967971
explicit_coordinate_names : bool, optional
968972
Transfer variable and constraint names to the solver (default: False)
969973
@@ -974,6 +978,8 @@ def solve_problem_from_model(
974978
with contextlib.ExitStack() as stack:
975979
if env is None:
976980
env_ = stack.enter_context(gurobipy.Env())
981+
elif isinstance(env, dict):
982+
env_ = stack.enter_context(gurobipy.Env(params=env))
977983
else:
978984
env_ = env
979985

@@ -998,7 +1004,7 @@ def solve_problem_from_file(
9981004
log_fn: Path | None = None,
9991005
warmstart_fn: Path | None = None,
10001006
basis_fn: Path | None = None,
1001-
env: None = None,
1007+
env: gurobipy.Env | dict[str, Any] | None = None,
10021008
) -> Result:
10031009
"""
10041010
Solve a linear problem from a problem file using the Gurobi solver.
@@ -1017,8 +1023,8 @@ def solve_problem_from_file(
10171023
Path to the warmstart file.
10181024
basis_fn : Path, optional
10191025
Path to the basis file.
1020-
env : None, optional
1021-
Gurobi environment for the solver
1026+
env : gurobipy.Env or dict, optional
1027+
Gurobi environment for the solver, pass env directly or kwargs for creation.
10221028
10231029
Returns
10241030
-------
@@ -1031,6 +1037,8 @@ def solve_problem_from_file(
10311037
with contextlib.ExitStack() as stack:
10321038
if env is None:
10331039
env_ = stack.enter_context(gurobipy.Env())
1040+
elif isinstance(env, dict):
1041+
env_ = stack.enter_context(gurobipy.Env(params=env))
10341042
else:
10351043
env_ = env
10361044

@@ -1150,7 +1158,7 @@ def get_solver_solution() -> Solution:
11501158
return Result(status, solution, m)
11511159

11521160

1153-
class Cplex(Solver):
1161+
class Cplex(Solver[None]):
11541162
"""
11551163
Solver subclass for the Cplex solver.
11561164
@@ -1306,7 +1314,7 @@ def get_solver_solution() -> Solution:
13061314
return Result(status, solution, m)
13071315

13081316

1309-
class SCIP(Solver):
1317+
class SCIP(Solver[None]):
13101318
"""
13111319
Solver subclass for the SCIP solver.
13121320
@@ -1459,7 +1467,7 @@ def get_solver_solution() -> Solution:
14591467
return Result(status, solution, m)
14601468

14611469

1462-
class Xpress(Solver):
1470+
class Xpress(Solver[None]):
14631471
"""
14641472
Solver subclass for the xpress solver.
14651473
@@ -1596,7 +1604,7 @@ def get_solver_solution() -> Solution:
15961604
mosek_bas_re = re.compile(r" (XL|XU)\s+([^ \t]+)\s+([^ \t]+)| (LL|UL|BS)\s+([^ \t]+)")
15971605

15981606

1599-
class Mosek(Solver):
1607+
class Mosek(Solver[None]):
16001608
"""
16011609
Solver subclass for the Mosek solver.
16021610
@@ -1926,7 +1934,7 @@ def get_solver_solution() -> Solution:
19261934
return Result(status, solution)
19271935

19281936

1929-
class COPT(Solver):
1937+
class COPT(Solver[None]):
19301938
"""
19311939
Solver subclass for the COPT solver.
19321940
@@ -2067,7 +2075,7 @@ def get_solver_solution() -> Solution:
20672075
return Result(status, solution, m)
20682076

20692077

2070-
class MindOpt(Solver):
2078+
class MindOpt(Solver[None]):
20712079
"""
20722080
Solver subclass for the MindOpt solver.
20732081
@@ -2210,7 +2218,7 @@ def get_solver_solution() -> Solution:
22102218
return Result(status, solution, m)
22112219

22122220

2213-
class PIPS(Solver):
2221+
class PIPS(Solver[None]):
22142222
"""
22152223
Solver subclass for the PIPS solver.
22162224
"""

test/test_solvers.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
from pathlib import Path
99

1010
import pytest
11+
from test_io import model # noqa: F401
1112

12-
from linopy import solvers
13+
from linopy import Model, solvers
1314

1415
free_mps_problem = """NAME sample_mip
1516
ROWS
@@ -64,3 +65,58 @@ def test_free_mps_solution_parsing(solver: str, tmp_path: Path) -> None:
6465

6566
assert result.status.is_ok
6667
assert result.solution.objective == 30.0
68+
69+
70+
@pytest.mark.skipif(
71+
"gurobi" not in set(solvers.available_solvers), reason="Gurobi is not installed"
72+
)
73+
def test_gurobi_environment_with_dict(model: Model, tmp_path: Path) -> None: # noqa: F811
74+
gurobi = solvers.Gurobi()
75+
76+
mps_file = tmp_path / "problem.mps"
77+
mps_file.write_text(free_mps_problem)
78+
sol_file = tmp_path / "solution.sol"
79+
80+
log1_file = tmp_path / "gurobi1.log"
81+
result = gurobi.solve_problem(
82+
problem_fn=mps_file, solution_fn=sol_file, env={"LogFile": str(log1_file)}
83+
)
84+
85+
assert result.status.is_ok
86+
assert log1_file.exists()
87+
88+
log2_file = tmp_path / "gurobi2.log"
89+
gurobi.solve_problem(
90+
model=model, solution_fn=sol_file, env={"LogFile": str(log2_file)}
91+
)
92+
assert result.status.is_ok
93+
assert log2_file.exists()
94+
95+
96+
@pytest.mark.skipif(
97+
"gurobi" not in set(solvers.available_solvers), reason="Gurobi is not installed"
98+
)
99+
def test_gurobi_environment_with_gurobi_env(model: Model, tmp_path: Path) -> None: # noqa: F811
100+
import gurobipy as gp
101+
102+
gurobi = solvers.Gurobi()
103+
104+
mps_file = tmp_path / "problem.mps"
105+
mps_file.write_text(free_mps_problem)
106+
sol_file = tmp_path / "solution.sol"
107+
108+
log1_file = tmp_path / "gurobi1.log"
109+
110+
with gp.Env(params={"LogFile": str(log1_file)}) as env:
111+
result = gurobi.solve_problem(
112+
problem_fn=mps_file, solution_fn=sol_file, env=env
113+
)
114+
115+
assert result.status.is_ok
116+
assert log1_file.exists()
117+
118+
log2_file = tmp_path / "gurobi2.log"
119+
with gp.Env(params={"LogFile": str(log2_file)}) as env:
120+
gurobi.solve_problem(model=model, solution_fn=sol_file, env=env)
121+
assert result.status.is_ok
122+
assert log2_file.exists()

0 commit comments

Comments
 (0)