pyoframe 0.0.5__tar.gz → 0.0.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. {pyoframe-0.0.5/src/pyoframe.egg-info → pyoframe-0.0.6}/PKG-INFO +1 -1
  2. {pyoframe-0.0.5 → pyoframe-0.0.6}/pyproject.toml +1 -1
  3. {pyoframe-0.0.5 → pyoframe-0.0.6}/src/pyoframe/_arithmetic.py +1 -1
  4. {pyoframe-0.0.5 → pyoframe-0.0.6}/src/pyoframe/constants.py +5 -2
  5. {pyoframe-0.0.5 → pyoframe-0.0.6}/src/pyoframe/core.py +23 -9
  6. {pyoframe-0.0.5 → pyoframe-0.0.6}/src/pyoframe/io.py +15 -5
  7. {pyoframe-0.0.5 → pyoframe-0.0.6}/src/pyoframe/io_mappers.py +20 -19
  8. {pyoframe-0.0.5 → pyoframe-0.0.6}/src/pyoframe/model.py +7 -6
  9. {pyoframe-0.0.5 → pyoframe-0.0.6}/src/pyoframe/model_element.py +6 -2
  10. {pyoframe-0.0.5 → pyoframe-0.0.6}/src/pyoframe/solvers.py +56 -32
  11. {pyoframe-0.0.5 → pyoframe-0.0.6/src/pyoframe.egg-info}/PKG-INFO +1 -1
  12. {pyoframe-0.0.5 → pyoframe-0.0.6}/LICENSE +0 -0
  13. {pyoframe-0.0.5 → pyoframe-0.0.6}/README.md +0 -0
  14. {pyoframe-0.0.5 → pyoframe-0.0.6}/setup.cfg +0 -0
  15. {pyoframe-0.0.5 → pyoframe-0.0.6}/src/pyoframe/__init__.py +0 -0
  16. {pyoframe-0.0.5 → pyoframe-0.0.6}/src/pyoframe/monkey_patch.py +0 -0
  17. {pyoframe-0.0.5 → pyoframe-0.0.6}/src/pyoframe/objective.py +0 -0
  18. {pyoframe-0.0.5 → pyoframe-0.0.6}/src/pyoframe/user_defined.py +0 -0
  19. {pyoframe-0.0.5 → pyoframe-0.0.6}/src/pyoframe/util.py +0 -0
  20. {pyoframe-0.0.5 → pyoframe-0.0.6}/src/pyoframe.egg-info/SOURCES.txt +0 -0
  21. {pyoframe-0.0.5 → pyoframe-0.0.6}/src/pyoframe.egg-info/dependency_links.txt +0 -0
  22. {pyoframe-0.0.5 → pyoframe-0.0.6}/src/pyoframe.egg-info/requires.txt +0 -0
  23. {pyoframe-0.0.5 → pyoframe-0.0.6}/src/pyoframe.egg-info/top_level.txt +0 -0
  24. {pyoframe-0.0.5 → pyoframe-0.0.6}/tests/test_arithmetic.py +0 -0
  25. {pyoframe-0.0.5 → pyoframe-0.0.6}/tests/test_examples.py +0 -0
  26. {pyoframe-0.0.5 → pyoframe-0.0.6}/tests/test_io.py +0 -0
  27. {pyoframe-0.0.5 → pyoframe-0.0.6}/tests/test_operations.py +0 -0
  28. {pyoframe-0.0.5 → pyoframe-0.0.6}/tests/test_solver.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyoframe
3
- Version: 0.0.5
3
+ Version: 0.0.6
4
4
  Summary: Blazing fast linear program interface
5
5
  Author-email: Bravos Power <dev@bravospower.com>
6
6
  Project-URL: Homepage, https://bravos-power.github.io/pyoframe/
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyoframe"
7
- version = "0.0.5"
7
+ version = "0.0.6"
8
8
  authors = [{ name = "Bravos Power", email = "dev@bravospower.com" }]
9
9
  description = "Blazing fast linear program interface"
10
10
  readme = "README.md"
@@ -106,7 +106,7 @@ def _add_expressions_core(*expressions: "Expression") -> "Expression":
106
106
 
107
107
  strat = (left.unmatched_strategy, right.unmatched_strategy)
108
108
 
109
- propogate_strat = propogatation_strategies[strat]
109
+ propogate_strat = propogatation_strategies[strat] # type: ignore
110
110
 
111
111
  if strat == (UnmatchedStrategy.DROP, UnmatchedStrategy.DROP):
112
112
  left_data = left.data.join(get_indices(right), how="inner", on=dims)
@@ -132,8 +132,11 @@ class SolverStatus(Enum):
132
132
  def from_termination_condition(
133
133
  cls, termination_condition: "TerminationCondition"
134
134
  ) -> "SolverStatus":
135
- for status in STATUS_TO_TERMINATION_CONDITION_MAP:
136
- if termination_condition in STATUS_TO_TERMINATION_CONDITION_MAP[status]:
135
+ for (
136
+ status,
137
+ termination_conditions,
138
+ ) in STATUS_TO_TERMINATION_CONDITION_MAP.items():
139
+ if termination_condition in termination_conditions:
137
140
  return status
138
141
  return cls("unknown")
139
142
 
@@ -689,9 +689,9 @@ class Expression(ModelElement, SupportsMath, SupportPolarsMethodMixin):
689
689
  >>> import pyoframe as pf
690
690
  >>> m = pf.Model("max")
691
691
  >>> m.X = pf.Variable({"dim1": [1, 2, 3]}, ub=10)
692
- >>> m.expr_1 = 2 * m.X
692
+ >>> m.expr_1 = 2 * m.X + 1
693
693
  >>> m.expr_2 = pf.sum(m.expr_1)
694
- >>> m.objective = m.expr_2 + 3
694
+ >>> m.objective = m.expr_2 - 3
695
695
  >>> result = m.solve(log_to_console=False)
696
696
  >>> m.expr_1.value
697
697
  shape: (3, 2)
@@ -700,15 +700,16 @@ class Expression(ModelElement, SupportsMath, SupportPolarsMethodMixin):
700
700
  │ --- ┆ --- │
701
701
  │ i64 ┆ f64 │
702
702
  ╞══════╪══════════╡
703
- │ 1 ┆ 20.0 │
704
- │ 2 ┆ 20.0 │
705
- │ 3 ┆ 20.0 │
703
+ │ 1 ┆ 21.0 │
704
+ │ 2 ┆ 21.0 │
705
+ │ 3 ┆ 21.0 │
706
706
  └──────┴──────────┘
707
707
  >>> m.expr_2.value
708
- 60.0
709
- >>> m.objective.value
710
708
  63.0
711
709
  """
710
+ assert (
711
+ self._model is not None
712
+ ), "Expression must be added to the model to use .value"
712
713
  if self._model.result is None or self._model.result.solution is None:
713
714
  raise ValueError(
714
715
  "Can't obtain value of expression since the model has not been solved."
@@ -716,8 +717,15 @@ class Expression(ModelElement, SupportsMath, SupportPolarsMethodMixin):
716
717
 
717
718
  df = (
718
719
  self.data.join(self._model.result.solution.primal, on=VAR_KEY, how="left")
720
+ .with_columns(
721
+ (
722
+ pl.when(pl.col(VAR_KEY) == CONST_TERM)
723
+ .then(1)
724
+ .otherwise(pl.col(SOLUTION_KEY))
725
+ * pl.col(COEF_KEY)
726
+ ).alias(SOLUTION_KEY)
727
+ )
719
728
  .drop(VAR_KEY)
720
- .with_columns((pl.col(SOLUTION_KEY) * pl.col(COEF_KEY)))
721
729
  .drop(COEF_KEY)
722
730
  )
723
731
 
@@ -924,6 +932,9 @@ class Constraint(ModelElementWithId):
924
932
  The first call to this property will load the slack values from the solver (lazy loading).
925
933
  """
926
934
  if SLACK_COL not in self.data.columns:
935
+ assert (
936
+ self._model is not None
937
+ ), "Constraint must be added to a model to get the slack."
927
938
  if self._model.solver is None:
928
939
  raise ValueError("The model has not been solved yet.")
929
940
  self._model.solver.load_slack()
@@ -1160,7 +1171,7 @@ class Variable(ModelElementWithId, SupportsMath, SupportPolarsMethodMixin):
1160
1171
  lb: float | int | SupportsToExpr | None = None,
1161
1172
  ub: float | int | SupportsToExpr | None = None,
1162
1173
  vtype: VType | VTypeValue = VType.CONTINUOUS,
1163
- equals: SupportsToExpr = None,
1174
+ equals: Optional[SupportsMath] = None,
1164
1175
  ):
1165
1176
  if lb is None:
1166
1177
  lb = float("-inf")
@@ -1222,6 +1233,9 @@ class Variable(ModelElementWithId, SupportsMath, SupportPolarsMethodMixin):
1222
1233
  The first call to this property will load the reduced costs from the solver (lazy loading).
1223
1234
  """
1224
1235
  if RC_COL not in self.data.columns:
1236
+ assert (
1237
+ self._model is not None
1238
+ ), "Variable must be added to a model to get the reduced cost."
1225
1239
  if self._model.solver is None:
1226
1240
  raise ValueError("The model has not been solved yet.")
1227
1241
  self._model.solver.load_rc()
@@ -11,8 +11,8 @@ from tqdm import tqdm
11
11
  from pyoframe.constants import CONST_TERM, VAR_KEY, ObjSense
12
12
  from pyoframe.core import Constraint, Variable
13
13
  from pyoframe.io_mappers import (
14
- Base62ConstMapper,
15
- Base62VarMapper,
14
+ Base36ConstMapper,
15
+ Base36VarMapper,
16
16
  IOMappers,
17
17
  Mapper,
18
18
  NamedMapper,
@@ -131,7 +131,7 @@ def get_var_map(m: "Model", use_var_names):
131
131
  return m.var_map
132
132
  var_map = NamedVariableMapper(Variable)
133
133
  else:
134
- var_map = Base62VarMapper(Variable)
134
+ var_map = Base36VarMapper(Variable)
135
135
 
136
136
  for v in m.variables:
137
137
  var_map.add(v)
@@ -139,10 +139,20 @@ def get_var_map(m: "Model", use_var_names):
139
139
 
140
140
 
141
141
  def to_file(
142
- m: "Model", file_path: Optional[Union[str, Path]], use_var_names=False
142
+ m: "Model", file_path: Optional[Union[str, Path]] = None, use_var_names=False
143
143
  ) -> Path:
144
144
  """
145
145
  Write out a model to a lp file.
146
+
147
+ Args:
148
+ m: The model to write out.
149
+ file_path: The path to write the model to. If None, a temporary file is created. The caller is responsible for
150
+ deleting the file after use.
151
+ use_var_names: If True, variable names are used in the lp file. Otherwise, variable
152
+ indices are used.
153
+
154
+ Returns:
155
+ The path to the lp file.
146
156
  """
147
157
  if file_path is None:
148
158
  with NamedTemporaryFile(
@@ -157,7 +167,7 @@ def to_file(
157
167
  file_path.unlink()
158
168
 
159
169
  const_map = (
160
- NamedMapper(Constraint) if use_var_names else Base62ConstMapper(Constraint)
170
+ NamedMapper(Constraint) if use_var_names else Base36ConstMapper(Constraint)
161
171
  )
162
172
  for c in m.constraints:
163
173
  const_map.add(c)
@@ -16,7 +16,7 @@ from pyoframe.constants import CONST_TERM
16
16
  if TYPE_CHECKING: # pragma: no cover
17
17
  from pyoframe.model import Variable
18
18
  from pyoframe.core import Constraint
19
- from pyoframe.util import CountableModelElement
19
+ from pyoframe.model_element import ModelElementWithId
20
20
 
21
21
 
22
22
  @dataclass
@@ -29,7 +29,7 @@ class Mapper(ABC):
29
29
 
30
30
  NAME_COL = "__name"
31
31
 
32
- def __init__(self, cls: Type["CountableModelElement"]) -> None:
32
+ def __init__(self, cls: Type["ModelElementWithId"]) -> None:
33
33
  self._ID_COL = cls.get_id_column_name()
34
34
  self.mapping_registry = pl.DataFrame(
35
35
  {self._ID_COL: [], Mapper.NAME_COL: []},
@@ -43,7 +43,7 @@ class Mapper(ABC):
43
43
  self.mapping_registry = pl.concat([self.mapping_registry, df])
44
44
 
45
45
  @abstractmethod
46
- def _element_to_map(self, element: "CountableModelElement") -> pl.DataFrame: ...
46
+ def _element_to_map(self, element: "ModelElementWithId") -> pl.DataFrame: ...
47
47
 
48
48
  def apply(
49
49
  self,
@@ -107,13 +107,14 @@ class NamedVariableMapper(NamedMapper):
107
107
  )
108
108
 
109
109
 
110
- class Base62Mapper(Mapper, ABC):
111
- # Mapping between a base 62 character and its integer value
110
+ class Base36Mapper(Mapper, ABC):
111
+ # Mapping between a base 36 character and its integer value
112
+ # Note: we must use only lowercase since Gurobi auto-converts variables that aren't in constraints to lowercase (kind of annoying)
112
113
  _CHAR_TABLE = pl.DataFrame(
113
- {"char": list(string.digits + string.ascii_letters)},
114
+ {"char": list(string.digits + string.ascii_lowercase)},
114
115
  ).with_columns(pl.int_range(pl.len()).cast(pl.UInt32).alias("code"))
115
116
 
116
- _BASE = _CHAR_TABLE.height # _BASE = 62
117
+ _BASE = _CHAR_TABLE.height # _BASE = 36
117
118
  _ZERO = _CHAR_TABLE.filter(pl.col("code") == 0).select("char").item() # _ZERO = "0"
118
119
 
119
120
  @property
@@ -131,7 +132,7 @@ class Base62Mapper(Mapper, ABC):
131
132
  query = pl.concat_str(
132
133
  pl.lit(self._prefix),
133
134
  pl.col(self._ID_COL).map_batches(
134
- Base62Mapper._to_base62,
135
+ Base36Mapper._to_base36,
135
136
  return_dtype=pl.String,
136
137
  is_elementwise=True,
137
138
  ),
@@ -143,24 +144,24 @@ class Base62Mapper(Mapper, ABC):
143
144
  return df.with_columns(query.alias(to_col))
144
145
 
145
146
  @classmethod
146
- def _to_base62(cls, int_col: pl.Series) -> pl.Series:
147
- """Returns a series of dtype str with a base 62 representation of the integers in int_col.
148
- The letters 0-9a-zA-Z are used as symbols for the representation.
147
+ def _to_base36(cls, int_col: pl.Series) -> pl.Series:
148
+ """Returns a series of dtype str with a base 36 representation of the integers in int_col.
149
+ The letters 0-9A-Z are used as symbols for the representation.
149
150
 
150
151
  Examples:
151
152
 
152
153
  >>> import polars as pl
153
154
  >>> s = pl.Series([0,10,20,60,53,66], dtype=pl.UInt32)
154
- >>> Base62Mapper._to_base62(s).to_list()
155
- ['0', 'a', 'k', 'Y', 'R', '14']
155
+ >>> Base36Mapper._to_base36(s).to_list()
156
+ ['0', 'a', 'k', '1o', '1h', '1u']
156
157
 
157
158
  >>> s = pl.Series([0], dtype=pl.UInt32)
158
- >>> Base62Mapper._to_base62(s).to_list()
159
+ >>> Base36Mapper._to_base36(s).to_list()
159
160
  ['0']
160
161
  """
161
162
  assert isinstance(
162
163
  int_col.dtype, pl.UInt32
163
- ), "_to_base62() only works for UInt32 id columns"
164
+ ), "_to_base36() only works for UInt32 id columns"
164
165
 
165
166
  largest_id = int_col.max()
166
167
  if largest_id == 0:
@@ -193,7 +194,7 @@ class Base62Mapper(Mapper, ABC):
193
194
  return self.apply(element.ids.select(self._ID_COL), to_col=Mapper.NAME_COL)
194
195
 
195
196
 
196
- class Base62VarMapper(Base62Mapper):
197
+ class Base36VarMapper(Base36Mapper):
197
198
  """
198
199
  Examples:
199
200
  >>> import polars as pl
@@ -203,10 +204,10 @@ class Base62VarMapper(Base62Mapper):
203
204
  >>> m.x = Variable(pl.DataFrame({"t": range(1,63)}))
204
205
  >>> (m.x.filter(t=11)+1).to_str()
205
206
  '[11]: 1 + x[11]'
206
- >>> (m.x.filter(t=11)+1).to_str(var_map=Base62VarMapper(Variable))
207
+ >>> (m.x.filter(t=11)+1).to_str(var_map=Base36VarMapper(Variable))
207
208
  '[11]: 1 + xb'
208
209
 
209
- >>> Base62VarMapper(Variable).apply(pl.DataFrame({VAR_KEY: []}))
210
+ >>> Base36VarMapper(Variable).apply(pl.DataFrame({VAR_KEY: []}))
210
211
  shape: (0, 1)
211
212
  ┌───────────────┐
212
213
  │ __variable_id │
@@ -230,7 +231,7 @@ class Base62VarMapper(Base62Mapper):
230
231
  return "x"
231
232
 
232
233
 
233
- class Base62ConstMapper(Base62Mapper):
234
+ class Base36ConstMapper(Base36Mapper):
234
235
 
235
236
  @property
236
237
  def _prefix(self) -> "str":
@@ -37,6 +37,7 @@ class Model(AttrContainerMixin):
37
37
  "result",
38
38
  "attr",
39
39
  "sense",
40
+ "objective",
40
41
  ]
41
42
 
42
43
  def __init__(self, min_or_max: Union[ObjSense, ObjSenseValue], name=None, **kwargs):
@@ -75,6 +76,12 @@ class Model(AttrContainerMixin):
75
76
  def objective(self):
76
77
  return self._objective
77
78
 
79
+ @objective.setter
80
+ def objective(self, value):
81
+ value = Objective(value)
82
+ self._objective = value
83
+ value.on_add_to_model(self, "objective")
84
+
78
85
  def __setattr__(self, __name: str, __value: Any) -> None:
79
86
  if __name not in Model._reserved_attributes and not isinstance(
80
87
  __value, (ModelElement, pl.DataFrame, pd.DataFrame)
@@ -87,9 +94,6 @@ class Model(AttrContainerMixin):
87
94
  isinstance(__value, ModelElement)
88
95
  and __name not in Model._reserved_attributes
89
96
  ):
90
- if __name == "objective":
91
- __value = Objective(__value)
92
-
93
97
  if isinstance(__value, ModelElementWithId):
94
98
  assert not hasattr(
95
99
  self, __name
@@ -103,9 +107,6 @@ class Model(AttrContainerMixin):
103
107
  self.var_map.add(__value)
104
108
  elif isinstance(__value, Constraint):
105
109
  self._constraints.append(__value)
106
- elif isinstance(__value, Objective):
107
- self._objective = __value
108
- return
109
110
  return super().__setattr__(__name, __value)
110
111
 
111
112
  def __repr__(self) -> str:
@@ -109,7 +109,7 @@ def _support_polars_method(method_name: str):
109
109
  Wrapper to add a method to ModelElement that simply calls the underlying Polars method on the data attribute.
110
110
  """
111
111
 
112
- def method(self: "SupportPolarsMethodMixin", *args, **kwargs):
112
+ def method(self: "SupportPolarsMethodMixin", *args, **kwargs) -> Any:
113
113
  result_from_polars = getattr(self.data, method_name)(*args, **kwargs)
114
114
  if isinstance(result_from_polars, pl.DataFrame):
115
115
  return self._new(result_from_polars)
@@ -119,7 +119,7 @@ def _support_polars_method(method_name: str):
119
119
  return method
120
120
 
121
121
 
122
- class SupportPolarsMethodMixin:
122
+ class SupportPolarsMethodMixin(ABC):
123
123
  rename = _support_polars_method("rename")
124
124
  with_columns = _support_polars_method("with_columns")
125
125
  filter = _support_polars_method("filter")
@@ -131,6 +131,10 @@ class SupportPolarsMethodMixin:
131
131
  Used to create a new instance of the same class with the given data (for e.g. on .rename(), .with_columns(), etc.).
132
132
  """
133
133
 
134
+ @property
135
+ @abstractmethod
136
+ def data(self): ...
137
+
134
138
 
135
139
  class ModelElementWithId(ModelElement, AttrContainerMixin):
136
140
  """
@@ -5,7 +5,7 @@ Code to interface with various solvers
5
5
  from abc import abstractmethod, ABC
6
6
  from functools import lru_cache
7
7
  from pathlib import Path
8
- from typing import Any, Dict, List, Optional, Type, Union, TYPE_CHECKING
8
+ from typing import Any, Dict, Optional, Type, Union, TYPE_CHECKING
9
9
 
10
10
  import polars as pl
11
11
 
@@ -50,7 +50,6 @@ def solve(
50
50
  solver=None,
51
51
  directory: Optional[Union[Path, str]] = None,
52
52
  use_var_names=False,
53
- env=None,
54
53
  log_fn=None,
55
54
  warmstart_fn=None,
56
55
  basis_fn=None,
@@ -68,8 +67,13 @@ def solve(
68
67
  raise ValueError(f"Solver {solver} not recognized or supported.")
69
68
 
70
69
  solver_cls = solver_registry[solver]
71
- m.solver = solver_cls(m, log_to_console)
72
- m.solver_model = m.solver.create_solver_model(directory, use_var_names, env)
70
+ m.solver = solver_cls(
71
+ m,
72
+ log_to_console,
73
+ params={param: value for param, value in m.params},
74
+ directory=directory,
75
+ )
76
+ m.solver_model = m.solver.create_solver_model(use_var_names)
73
77
  m.solver.solver_model = m.solver_model
74
78
 
75
79
  for attr_container in [m.variables, m.constraints, [m]]:
@@ -77,9 +81,6 @@ def solve(
77
81
  for param_name, param_value in container.attr:
78
82
  m.solver.set_attr(container, param_name, param_value)
79
83
 
80
- for param, value in m.params:
81
- m.solver.set_param(param, value)
82
-
83
84
  result = m.solver.solve(log_fn, warmstart_fn, basis_fn, solution_file)
84
85
  result = m.solver.process_result(result)
85
86
  m.result = result
@@ -99,20 +100,19 @@ def solve(
99
100
 
100
101
 
101
102
  class Solver(ABC):
102
- def __init__(self, model, log_to_console):
103
+ def __init__(self, model: "Model", log_to_console, params, directory):
103
104
  self._model = model
104
105
  self.solver_model: Optional[Any] = None
105
- self.log_to_console = log_to_console
106
+ self.log_to_console: bool = log_to_console
107
+ self.params = params
108
+ self.directory = directory
106
109
 
107
110
  @abstractmethod
108
- def create_solver_model(self) -> Any: ...
111
+ def create_solver_model(self, use_var_names) -> Any: ...
109
112
 
110
113
  @abstractmethod
111
114
  def set_attr(self, element, param_name, param_value): ...
112
115
 
113
- @abstractmethod
114
- def set_param(self, param_name, param_value): ...
115
-
116
116
  @abstractmethod
117
117
  def solve(self, log_fn, warmstart_fn, basis_fn, solution_file) -> Result: ...
118
118
 
@@ -135,12 +135,25 @@ class Solver(ABC):
135
135
  @abstractmethod
136
136
  def _get_all_slack(self): ...
137
137
 
138
+ def dispose(self):
139
+ """
140
+ Clean up any resources that wouldn't be cleaned up by the garbage collector.
141
+
142
+ For now, this is only used by the Gurobi solver to call .dispose() on the solver model and Gurobi environment
143
+ which helps close a connection to the Gurobi Computer Server. Note that this effectively disables commands that
144
+ need access to the solver model (like .slack and .RC)
145
+ """
146
+
138
147
 
139
148
  class FileBasedSolver(Solver):
140
- def create_solver_model(
141
- self, directory: Optional[Union[Path, str]], use_var_names, env
142
- ) -> Any:
149
+ def __init__(self, *args, **kwargs):
150
+ super().__init__(*args, **kwargs)
151
+ self.problem_file: Optional[Path] = None
152
+ self.keep_files = self.directory is not None
153
+
154
+ def create_solver_model(self, use_var_names) -> Any:
143
155
  problem_file = None
156
+ directory = self.directory
144
157
  if directory is not None:
145
158
  if isinstance(directory, str):
146
159
  directory = Path(directory)
@@ -150,12 +163,14 @@ class FileBasedSolver(Solver):
150
163
  self._model.name if self._model.name is not None else "pyoframe-problem"
151
164
  )
152
165
  problem_file = directory / f"{filename}.lp"
153
- problem_file = self._model.to_file(problem_file, use_var_names=use_var_names)
166
+ self.problem_file = self._model.to_file(
167
+ problem_file, use_var_names=use_var_names
168
+ )
154
169
  assert self._model.io_mappers is not None
155
- return self.create_solver_model_from_lp(problem_file, env)
170
+ return self.create_solver_model_from_lp()
156
171
 
157
172
  @abstractmethod
158
- def create_solver_model_from_lp(self, problem_file: Path, env) -> Any: ...
173
+ def create_solver_model_from_lp(self) -> Any: ...
159
174
 
160
175
  def set_attr(self, element, param_name, param_value):
161
176
  if isinstance(param_value, pl.DataFrame):
@@ -216,30 +231,30 @@ class GurobiSolver(FileBasedSolver):
216
231
  17: "internal_solver_error",
217
232
  }
218
233
 
219
- def create_solver_model_from_lp(self, problem_fn, env) -> Result:
234
+ def __init__(self, *args, **kwargs):
235
+ super().__init__(*args, **kwargs)
236
+ if not self.log_to_console:
237
+ self.params["LogToConsole"] = 0
238
+ self.env = None
239
+
240
+ def create_solver_model_from_lp(self) -> Any:
220
241
  """
221
242
  Solve a linear problem using the gurobi solver.
222
243
 
223
244
  This function communicates with gurobi using the gurubipy package.
224
245
  """
246
+ assert self.problem_file is not None
247
+ self.env = gurobipy.Env(params=self.params)
225
248
 
226
- if env is None:
227
- if self.log_to_console:
228
- env = gurobipy.Env()
229
- else:
230
- # See https://support.gurobi.com/hc/en-us/articles/360044784552-How-do-I-suppress-all-console-output-from-Gurobi
231
- env = gurobipy.Env(empty=True)
232
- env.setParam("LogToConsole", 0)
233
- env.start()
249
+ m = gurobipy.read(_path_to_str(self.problem_file), env=self.env)
250
+ if not self.keep_files:
251
+ self.problem_file.unlink()
234
252
 
235
- m = gurobipy.read(_path_to_str(problem_fn), env=env)
236
253
  return m
237
254
 
238
- def set_param(self, param_name, param_value):
239
- self.solver_model.setParam(param_name, param_value)
240
-
241
255
  @lru_cache
242
256
  def _get_var_mapping(self):
257
+ assert self.solver_model is not None
243
258
  vars = self.solver_model.getVars()
244
259
  return vars, pl.DataFrame(
245
260
  {VAR_KEY: self.solver_model.getAttr("VarName", vars)}
@@ -247,12 +262,14 @@ class GurobiSolver(FileBasedSolver):
247
262
 
248
263
  @lru_cache
249
264
  def _get_constraint_mapping(self):
265
+ assert self.solver_model is not None
250
266
  constraints = self.solver_model.getConstrs()
251
267
  return constraints, pl.DataFrame(
252
268
  {CONSTRAINT_KEY: self.solver_model.getAttr("ConstrName", constraints)}
253
269
  ).with_columns(i=pl.int_range(pl.len()))
254
270
 
255
271
  def set_attr_unmapped(self, element, param_name, param_value):
272
+ assert self.solver_model is not None
256
273
  if isinstance(element, pf.Model):
257
274
  self.solver_model.setAttr(param_name, param_value)
258
275
  elif isinstance(element, pf.Variable):
@@ -277,6 +294,7 @@ class GurobiSolver(FileBasedSolver):
277
294
  raise ValueError(f"Element type {type(element)} not recognized.")
278
295
 
279
296
  def solve(self, log_fn, warmstart_fn, basis_fn, solution_file) -> Result:
297
+ assert self.solver_model is not None
280
298
  m = self.solver_model
281
299
  if log_fn is not None:
282
300
  m.setParam("logfile", _path_to_str(log_fn))
@@ -345,6 +363,12 @@ class GurobiSolver(FileBasedSolver):
345
363
  }
346
364
  )
347
365
 
366
+ def dispose(self):
367
+ if self.solver_model is not None:
368
+ self.solver_model.dispose()
369
+ if self.env is not None:
370
+ self.env.dispose()
371
+
348
372
 
349
373
  def _path_to_str(path: Union[Path, str]) -> str:
350
374
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyoframe
3
- Version: 0.0.5
3
+ Version: 0.0.6
4
4
  Summary: Blazing fast linear program interface
5
5
  Author-email: Bravos Power <dev@bravospower.com>
6
6
  Project-URL: Homepage, https://bravos-power.github.io/pyoframe/
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes