Skip to content

Commit

Permalink
Auto detect installed solvers
Browse files Browse the repository at this point in the history
  • Loading branch information
staadecker committed Dec 9, 2024
1 parent b0e44fc commit f7c1a7f
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 40 deletions.
12 changes: 3 additions & 9 deletions src/pyoframe/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,8 @@
CONSTRAINT_KEY = "__constraint_id"
SOLUTION_KEY = "solution"
DUAL_KEY = "dual"

COL_DTYPES = {
COEF_KEY: pl.Float64,
VAR_KEY: pl.UInt32,
CONSTRAINT_KEY: pl.UInt32,
SOLUTION_KEY: pl.Float64,
}
VAR_TYPE = COL_DTYPES[VAR_KEY]
SUPPORTED_SOLVER_TYPES = Literal["gurobi", "highs"]
KEY_TYPE = pl.UInt32

# Variable ID for constant terms. This variable ID is reserved.
CONST_TERM = 0
Expand All @@ -47,7 +41,7 @@ def __init__(cls, name, bases, dct):


class Config(metaclass=_ConfigMeta):
default_solver = "gurobi"
default_solver: Optional[SUPPORTED_SOLVER_TYPES] = None
disable_unmatched_checks: bool = False
float_to_str_precision: Optional[int] = 5
print_uses_variable_names: bool = True
Expand Down
23 changes: 11 additions & 12 deletions src/pyoframe/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,14 @@
from pyoframe._arithmetic import _add_expressions, _get_dimensions
from pyoframe.constants import (
COEF_KEY,
COL_DTYPES,
CONST_TERM,
CONSTRAINT_KEY,
DUAL_KEY,
KEY_TYPE,
POLARS_VERSION,
RESERVED_COL_KEYS,
SOLUTION_KEY,
VAR_KEY,
VAR_TYPE,
Config,
ConstraintSense,
ObjSense,
Expand Down Expand Up @@ -698,7 +697,7 @@ def _add_const(self, const: int | float) -> Expression:
data,
pl.DataFrame(
{COEF_KEY: [0.0], VAR_KEY: [CONST_TERM]},
schema={COEF_KEY: pl.Float64, VAR_KEY: VAR_TYPE},
schema={COEF_KEY: pl.Float64, VAR_KEY: KEY_TYPE},
),
],
how="vertical_relaxed",
Expand All @@ -707,7 +706,7 @@ def _add_const(self, const: int | float) -> Expression:
keys = (
data.select(dim)
.unique(maintain_order=True)
.with_columns(pl.lit(CONST_TERM).alias(VAR_KEY).cast(VAR_TYPE))
.with_columns(pl.lit(CONST_TERM).alias(VAR_KEY).cast(KEY_TYPE))
)
if POLARS_VERSION.major >= 1:
data = data.join(keys, on=dim + [VAR_KEY], how="full", coalesce=True)
Expand Down Expand Up @@ -738,7 +737,7 @@ def constant_terms(self):
if len(constant_terms) == 0:
return pl.DataFrame(
{COEF_KEY: [0.0], VAR_KEY: [CONST_TERM]},
schema={COEF_KEY: pl.Float64, VAR_KEY: VAR_TYPE},
schema={COEF_KEY: pl.Float64, VAR_KEY: KEY_TYPE},
)
return constant_terms

Expand Down Expand Up @@ -787,7 +786,7 @@ def evaluate(self) -> pl.DataFrame:
lambda v_id: sm.get_variable_attribute(
poi.VariableIndex(v_id), attr
),
return_dtype=COL_DTYPES[SOLUTION_KEY],
return_dtype=pl.Float64,
)
).alias(COEF_KEY)
)
Expand Down Expand Up @@ -1035,7 +1034,7 @@ def _assign_ids(self):
).index
)
.alias(CONSTRAINT_KEY)
.cast(VAR_TYPE)
.cast(KEY_TYPE)
)
else:
df = self.lhs.data.group_by(self.dimensions, maintain_order=True).agg(
Expand All @@ -1058,7 +1057,7 @@ def _assign_ids(self):
name=x["concated_dim"],
**kwargs,
).index,
return_dtype=VAR_TYPE,
return_dtype=KEY_TYPE,
)
.alias(CONSTRAINT_KEY),
)
Expand All @@ -1075,7 +1074,7 @@ def _assign_ids(self):
),
**kwargs,
).index,
return_dtype=VAR_TYPE,
return_dtype=KEY_TYPE,
)
.alias(CONSTRAINT_KEY),
)
Expand Down Expand Up @@ -1355,7 +1354,7 @@ def _assign_ids(self):
lambda name: self._model.poi.add_variable(
name=name, **kwargs
).index,
return_dtype=VAR_TYPE,
return_dtype=KEY_TYPE,
)
.alias(VAR_KEY)
)
Expand All @@ -1366,11 +1365,11 @@ def _assign_ids(self):
kwargs["name"] = self.name

df = self.data.with_columns(
pl.lit(0).alias(VAR_KEY).cast(VAR_TYPE)
pl.lit(0).alias(VAR_KEY).cast(KEY_TYPE)
).with_columns(
pl.col(VAR_KEY).map_elements(
lambda _: self._model.poi.add_variable(**kwargs).index,
return_dtype=VAR_TYPE,
return_dtype=KEY_TYPE,
)
)

Expand Down
72 changes: 53 additions & 19 deletions src/pyoframe/model.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from pathlib import Path
from typing import Any, Iterable, List, Optional, Union
from typing import Any, Dict, Iterable, List, Optional, Union

import pandas as pd
import polars as pl
import pyoptinterface as poi

from pyoframe.constants import (
CONST_TERM,
SUPPORTED_SOLVER_TYPES,
Config,
ObjSense,
ObjSenseValue,
Expand All @@ -21,7 +22,20 @@

class Model:
"""
Represents a mathematical optimization model. Add variables, constraints, and an objective to the model by setting attributes.
The object that holds all the variables, constraints, and the objective.
Parameters:
min_or_max:
The sense of the objective. Either "min" or "max".
name:
The name of the model. Currently not used for much.
solver:
The solver to use. If `None`, `Config.default_solver` will be used.
If `Config.default_solver` is `None`, the first solver that imports without an error will be used.
solver_env:
Gurobi only: a dictionary of parameters to set when creating the Gurobi environment.
use_var_names:
Whether to pass variable names to the solver. Set to `True` if you'd like outputs from e.g. `Model.write()` to be legible.
Example:
>>> m = pf.Model()
Expand Down Expand Up @@ -54,15 +68,11 @@ def __init__(
self,
min_or_max: Union[ObjSense, ObjSenseValue] = "min",
name=None,
solver: Optional[str] = None,
solver: Optional[SUPPORTED_SOLVER_TYPES] = None,
solver_env: Optional[Dict[str, str]] = None,
use_var_names=False,
**kwargs,
):
super().__init__(**kwargs)
if solver is None:
solver = Config.default_solver
self.solver_name: str = solver
self.poi: Optional["poi.gurobi.Model"] = Model.create_pyoptint_model(solver)
self.poi, self.solver_name = Model.create_poi_model(solver, solver_env)
self._variables: List[Variable] = []
self._constraints: List[Constraint] = []
self.sense = ObjSense(min_or_max)
Expand All @@ -80,26 +90,50 @@ def __init__(
def use_var_names(self):
return self._use_var_names

@staticmethod
def create_pyoptint_model(solver: str):
@classmethod
def create_poi_model(
cls, solver: Optional[str], solver_env: Optional[Dict[str, str]]
):
if solver is None:
if Config.default_solver is None:
for solver_option in ["highs", "gurobi"]:
try:
return cls.create_poi_model(solver_option, solver_env)
except RuntimeError:
pass
raise ValueError(

Check warning on line 104 in src/pyoframe/model.py

View check run for this annotation

Codecov / codecov/patch

src/pyoframe/model.py#L102-L104

Added lines #L102 - L104 were not covered by tests
'Could not automatically find a solver. Is one installed? If so, specify which one: e.g. Model(solver="gurobi")'
)
else:
solver = Config.default_solver

solver = solver.lower()
if solver == "gurobi":
from pyoptinterface.gurobi import Model
from pyoptinterface import gurobi

if solver_env is None:
model = gurobi.Model()
else:
env = gurobi.Env(empty=True)
for key, value in solver_env.items():
env.set_raw_parameter(key, value)
env.start()
model = gurobi.Model(env)

Check warning on line 121 in src/pyoframe/model.py

View check run for this annotation

Codecov / codecov/patch

src/pyoframe/model.py#L117-L121

Added lines #L117 - L121 were not covered by tests
elif solver == "highs":
from pyoptinterface.highs import Model
# TODO add support for copt
# elif solver == "copt":
# from pyoptinterface.copt import Model
from pyoptinterface import highs

model = highs.Model()
else:
raise ValueError(
f"Solver {solver} not recognized or supported."
) # pragma: no cover
model = Model()

constant_var = model.add_variable(lb=1, ub=1, name="ONE")
if constant_var.index != CONST_TERM:
raise ValueError(
"The first variable should have index 0."
) # pragma: no cover
return model
return model, solver

@property
def variables(self) -> List[Variable]:
Expand All @@ -124,7 +158,7 @@ def integer_variables(self) -> Iterable[Variable]:
>>> m = pf.Model()
>>> m.X = pf.Variable(vtype=pf.VType.INTEGER)
>>> m.Y = pf.Variable()
>>> len(list(m.binary_variables))
>>> len(list(m.integer_variables))
1
"""
return (v for v in self.variables if v.vtype == VType.INTEGER)
Expand Down

0 comments on commit f7c1a7f

Please sign in to comment.