pyoframe 0.0.5__py3-none-any.whl → 0.0.7__py3-none-any.whl

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.
pyoframe/_arithmetic.py CHANGED
@@ -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)
pyoframe/constants.py CHANGED
@@ -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
 
pyoframe/core.py CHANGED
@@ -271,7 +271,11 @@ class Set(ModelElement, SupportsMath, SupportPolarsMethodMixin):
271
271
  elif isinstance(set, Constraint):
272
272
  df = set.data.select(set.dimensions_unsafe)
273
273
  elif isinstance(set, SupportsMath):
274
- df = set.to_expr().data.drop(RESERVED_COL_KEYS).unique(maintain_order=True)
274
+ df = (
275
+ set.to_expr()
276
+ .data.drop(RESERVED_COL_KEYS, strict=False)
277
+ .unique(maintain_order=True)
278
+ )
275
279
  elif isinstance(set, pd.Index):
276
280
  df = pl.from_pandas(pd.DataFrame(index=set).reset_index())
277
281
  elif isinstance(set, pd.DataFrame):
@@ -689,9 +693,9 @@ class Expression(ModelElement, SupportsMath, SupportPolarsMethodMixin):
689
693
  >>> import pyoframe as pf
690
694
  >>> m = pf.Model("max")
691
695
  >>> m.X = pf.Variable({"dim1": [1, 2, 3]}, ub=10)
692
- >>> m.expr_1 = 2 * m.X
696
+ >>> m.expr_1 = 2 * m.X + 1
693
697
  >>> m.expr_2 = pf.sum(m.expr_1)
694
- >>> m.objective = m.expr_2 + 3
698
+ >>> m.objective = m.expr_2 - 3
695
699
  >>> result = m.solve(log_to_console=False)
696
700
  >>> m.expr_1.value
697
701
  shape: (3, 2)
@@ -700,15 +704,16 @@ class Expression(ModelElement, SupportsMath, SupportPolarsMethodMixin):
700
704
  │ --- ┆ --- │
701
705
  │ i64 ┆ f64 │
702
706
  ╞══════╪══════════╡
703
- │ 1 ┆ 20.0 │
704
- │ 2 ┆ 20.0 │
705
- │ 3 ┆ 20.0 │
707
+ │ 1 ┆ 21.0 │
708
+ │ 2 ┆ 21.0 │
709
+ │ 3 ┆ 21.0 │
706
710
  └──────┴──────────┘
707
711
  >>> m.expr_2.value
708
- 60.0
709
- >>> m.objective.value
710
712
  63.0
711
713
  """
714
+ assert (
715
+ self._model is not None
716
+ ), "Expression must be added to the model to use .value"
712
717
  if self._model.result is None or self._model.result.solution is None:
713
718
  raise ValueError(
714
719
  "Can't obtain value of expression since the model has not been solved."
@@ -716,8 +721,15 @@ class Expression(ModelElement, SupportsMath, SupportPolarsMethodMixin):
716
721
 
717
722
  df = (
718
723
  self.data.join(self._model.result.solution.primal, on=VAR_KEY, how="left")
724
+ .with_columns(
725
+ (
726
+ pl.when(pl.col(VAR_KEY) == CONST_TERM)
727
+ .then(1)
728
+ .otherwise(pl.col(SOLUTION_KEY))
729
+ * pl.col(COEF_KEY)
730
+ ).alias(SOLUTION_KEY)
731
+ )
719
732
  .drop(VAR_KEY)
720
- .with_columns((pl.col(SOLUTION_KEY) * pl.col(COEF_KEY)))
721
733
  .drop(COEF_KEY)
722
734
  )
723
735
 
@@ -924,6 +936,9 @@ class Constraint(ModelElementWithId):
924
936
  The first call to this property will load the slack values from the solver (lazy loading).
925
937
  """
926
938
  if SLACK_COL not in self.data.columns:
939
+ assert (
940
+ self._model is not None
941
+ ), "Constraint must be added to a model to get the slack."
927
942
  if self._model.solver is None:
928
943
  raise ValueError("The model has not been solved yet.")
929
944
  self._model.solver.load_slack()
@@ -1160,7 +1175,7 @@ class Variable(ModelElementWithId, SupportsMath, SupportPolarsMethodMixin):
1160
1175
  lb: float | int | SupportsToExpr | None = None,
1161
1176
  ub: float | int | SupportsToExpr | None = None,
1162
1177
  vtype: VType | VTypeValue = VType.CONTINUOUS,
1163
- equals: SupportsToExpr = None,
1178
+ equals: Optional[SupportsMath] = None,
1164
1179
  ):
1165
1180
  if lb is None:
1166
1181
  lb = float("-inf")
@@ -1222,6 +1237,9 @@ class Variable(ModelElementWithId, SupportsMath, SupportPolarsMethodMixin):
1222
1237
  The first call to this property will load the reduced costs from the solver (lazy loading).
1223
1238
  """
1224
1239
  if RC_COL not in self.data.columns:
1240
+ assert (
1241
+ self._model is not None
1242
+ ), "Variable must be added to a model to get the reduced cost."
1225
1243
  if self._model.solver is None:
1226
1244
  raise ValueError("The model has not been solved yet.")
1227
1245
  self._model.solver.load_rc()
@@ -1245,7 +1263,7 @@ class Variable(ModelElementWithId, SupportsMath, SupportPolarsMethodMixin):
1245
1263
  )
1246
1264
 
1247
1265
  def to_expr(self) -> Expression:
1248
- return self._new(self.data.drop(SOLUTION_KEY))
1266
+ return self._new(self.data.drop(SOLUTION_KEY, strict=False))
1249
1267
 
1250
1268
  def _new(self, data: pl.DataFrame):
1251
1269
  e = Expression(data.with_columns(pl.lit(1.0).alias(COEF_KEY)))
@@ -1323,5 +1341,5 @@ class Variable(ModelElementWithId, SupportsMath, SupportPolarsMethodMixin):
1323
1341
  data = expr.data.rename({dim: "__prev"})
1324
1342
  data = data.join(
1325
1343
  wrapped, left_on="__prev", right_on="__next", how="inner"
1326
- ).drop(["__prev", "__next"])
1344
+ ).drop(["__prev", "__next"], strict=False)
1327
1345
  return expr._new(data)
pyoframe/io.py CHANGED
@@ -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)
pyoframe/io_mappers.py CHANGED
@@ -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":
pyoframe/model.py CHANGED
@@ -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:
pyoframe/model_element.py CHANGED
@@ -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
  """
pyoframe/solvers.py CHANGED
@@ -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))
@@ -295,7 +313,7 @@ class GurobiSolver(FileBasedSolver):
295
313
  termination_condition = GurobiSolver.CONDITION_MAP.get(condition, condition)
296
314
  status = Status.from_termination_condition(termination_condition)
297
315
 
298
- if status.is_ok:
316
+ if status.is_ok and (termination_condition == "optimal"):
299
317
  if solution_file:
300
318
  m.write(_path_to_str(solution_file))
301
319
 
@@ -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.7
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/
@@ -0,0 +1,18 @@
1
+ pyoframe/__init__.py,sha256=D7HHQPy2Me-LLyfPCcSE74dn83PeMK3aOby7i7oiLTs,507
2
+ pyoframe/_arithmetic.py,sha256=riyN2JC-BnOgTIxGfXKIS-X_p7zm8JaKrLk_KDwKAAw,9046
3
+ pyoframe/constants.py,sha256=olao24mkzQfwTlc5o4rQ-AOOTytmtVqNCFwJIc8zbds,7169
4
+ pyoframe/core.py,sha256=TwpWPYl1K5qDSFAX9yfabFKpahxgqoa2eL0zmkP91dI,51247
5
+ pyoframe/io.py,sha256=7bd_oKD8EZsuM0ONUAA9BDFSFvUoXLWGXDhnUqMde8o,5235
6
+ pyoframe/io_mappers.py,sha256=Op5451Yo4gNa-2BiPPCAjPYdFLo8jIeKHCYXUcGPRug,7385
7
+ pyoframe/model.py,sha256=xod3hSf__WWDy0V9pao9wPlQTc7-7x56FJoKKidsMbw,3768
8
+ pyoframe/model_element.py,sha256=H2gZxksb3UQ25vIdNlb07bCx3ZcWh7YD6-ViPVJV-JI,7691
9
+ pyoframe/monkey_patch.py,sha256=S_DU7cieU5C3t3kAyKQrGyLTwno0WANpDBV3xn7AyG8,2068
10
+ pyoframe/objective.py,sha256=JzuyMAQZ2OxEoAaK-splWwZei2hHPbCLdG-X2-yRkD0,1338
11
+ pyoframe/solvers.py,sha256=yf-hzUHDvKgmIHk2FobmygzE9-LnOzTBL5ps-nqGo8I,11875
12
+ pyoframe/user_defined.py,sha256=UWZSTpFj0a8n1_RHwC8Ubwqr4FO-gRPBqqfNUut1IZg,1717
13
+ pyoframe/util.py,sha256=KJubFV66E7WPI5UhcuUNsVwCm7WOcQBiLN1af1MAAgA,9647
14
+ pyoframe-0.0.7.dist-info/LICENSE,sha256=L1pXz6p_1OW5XGWb2UCR6PNu6k3JAT0XWhi8jV0cuRg,1137
15
+ pyoframe-0.0.7.dist-info/METADATA,sha256=zKjbfnByq8u6GRauys4BbRuWjL6SGiCmd9SBhwTDFCE,3445
16
+ pyoframe-0.0.7.dist-info/WHEEL,sha256=y4mX-SOX4fYIkonsAGA5N0Oy-8_gI4FXw5HNI1xqvWg,91
17
+ pyoframe-0.0.7.dist-info/top_level.txt,sha256=10z3OOJSVLriQ0IrFLMH8CH9zByugPWolqhlHlkNjV4,9
18
+ pyoframe-0.0.7.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.43.0)
2
+ Generator: setuptools (70.2.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,18 +0,0 @@
1
- pyoframe/__init__.py,sha256=D7HHQPy2Me-LLyfPCcSE74dn83PeMK3aOby7i7oiLTs,507
2
- pyoframe/_arithmetic.py,sha256=MIUSuvhGjBixzQCX-B7D8pMXwDE-d0Q4_f9vPNHAzys,9030
3
- pyoframe/constants.py,sha256=KZJaHRlvGvhIoZbjqNEc5UdXmjK8RgsXG-nVFAJ7sws,7121
4
- pyoframe/core.py,sha256=uBRfy4gpG9B08agjCo4nX6gCEUw1rz33X68Q6UQy-QM,50583
5
- pyoframe/io.py,sha256=2YNo6SJkbTwAIseCZU10wCrbXNexXYGU6sq6ET895Ds,4853
6
- pyoframe/io_mappers.py,sha256=xWvWarwzudMvJ1L70l-xSQgFCwHonmC0th5AqilTpIE,7249
7
- pyoframe/model.py,sha256=5ATWB-04UurPHiEuiSjSxB7CVWsdvj4el76dcQmHAyo,3776
8
- pyoframe/model_element.py,sha256=N9P56YbD_iscrgRk4yT-oW7pSuwMFDdjxxKVPvMwJ8U,7620
9
- pyoframe/monkey_patch.py,sha256=S_DU7cieU5C3t3kAyKQrGyLTwno0WANpDBV3xn7AyG8,2068
10
- pyoframe/objective.py,sha256=JzuyMAQZ2OxEoAaK-splWwZei2hHPbCLdG-X2-yRkD0,1338
11
- pyoframe/solvers.py,sha256=b9nSw5k-ko4eiA5Ad3z4vlcgp-pTyNeIWHQ7N4zV_AE,10994
12
- pyoframe/user_defined.py,sha256=UWZSTpFj0a8n1_RHwC8Ubwqr4FO-gRPBqqfNUut1IZg,1717
13
- pyoframe/util.py,sha256=KJubFV66E7WPI5UhcuUNsVwCm7WOcQBiLN1af1MAAgA,9647
14
- pyoframe-0.0.5.dist-info/LICENSE,sha256=L1pXz6p_1OW5XGWb2UCR6PNu6k3JAT0XWhi8jV0cuRg,1137
15
- pyoframe-0.0.5.dist-info/METADATA,sha256=FV-aSYBS5nMnegxZgtdzEDRFZZth4DJDdo_KYfjwbOQ,3445
16
- pyoframe-0.0.5.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
17
- pyoframe-0.0.5.dist-info/top_level.txt,sha256=10z3OOJSVLriQ0IrFLMH8CH9zByugPWolqhlHlkNjV4,9
18
- pyoframe-0.0.5.dist-info/RECORD,,