pyoframe 0.0.4__py3-none-any.whl → 0.0.5__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/solvers.py CHANGED
@@ -3,35 +3,90 @@ Code to interface with various solvers
3
3
  """
4
4
 
5
5
  from abc import abstractmethod, ABC
6
+ from functools import lru_cache
6
7
  from pathlib import Path
7
- from typing import Optional, Union, TYPE_CHECKING
8
+ from typing import Any, Dict, List, Optional, Type, Union, TYPE_CHECKING
8
9
 
9
10
  import polars as pl
10
11
 
11
12
  from pyoframe.constants import (
12
13
  DUAL_KEY,
13
- NAME_COL,
14
14
  SOLUTION_KEY,
15
+ SLACK_COL,
16
+ RC_COL,
17
+ VAR_KEY,
18
+ CONSTRAINT_KEY,
15
19
  Result,
16
20
  Solution,
17
21
  Status,
18
22
  )
19
23
  import contextlib
24
+ import pyoframe as pf
20
25
 
21
26
  from pathlib import Path
22
27
 
23
28
  if TYPE_CHECKING: # pragma: no cover
24
29
  from pyoframe.model import Model
25
30
 
31
+ available_solvers = []
32
+ solver_registry: Dict[str, Type["Solver"]] = {}
26
33
 
27
- def solve(m: "Model", solver, **kwargs):
28
- if solver == "gurobi":
29
- result = GurobiSolver().solve(m, **kwargs)
30
- else:
34
+ with contextlib.suppress(ImportError):
35
+ import gurobipy
36
+
37
+ available_solvers.append("gurobi")
38
+
39
+
40
+ def _register_solver(solver_name):
41
+ def decorator(cls):
42
+ solver_registry[solver_name] = cls
43
+ return cls
44
+
45
+ return decorator
46
+
47
+
48
+ def solve(
49
+ m: "Model",
50
+ solver=None,
51
+ directory: Optional[Union[Path, str]] = None,
52
+ use_var_names=False,
53
+ env=None,
54
+ log_fn=None,
55
+ warmstart_fn=None,
56
+ basis_fn=None,
57
+ solution_file=None,
58
+ log_to_console=True,
59
+ ):
60
+ if solver is None:
61
+ if len(available_solvers) == 0:
62
+ raise ValueError(
63
+ "No solvers available. Please install a solving library like gurobipy."
64
+ )
65
+ solver = available_solvers[0]
66
+
67
+ if solver not in solver_registry:
31
68
  raise ValueError(f"Solver {solver} not recognized or supported.")
32
69
 
70
+ 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)
73
+ m.solver.solver_model = m.solver_model
74
+
75
+ for attr_container in [m.variables, m.constraints, [m]]:
76
+ for container in attr_container:
77
+ for param_name, param_value in container.attr:
78
+ m.solver.set_attr(container, param_name, param_value)
79
+
80
+ for param, value in m.params:
81
+ m.solver.set_param(param, value)
82
+
83
+ result = m.solver.solve(log_fn, warmstart_fn, basis_fn, solution_file)
84
+ result = m.solver.process_result(result)
85
+ m.result = result
86
+
33
87
  if result.solution is not None:
34
- m.objective.value = result.solution.objective
88
+ if m.objective is not None:
89
+ m.objective.value = result.solution.objective
35
90
 
36
91
  for variable in m.variables:
37
92
  variable.solution = result.solution.primal
@@ -44,142 +99,254 @@ def solve(m: "Model", solver, **kwargs):
44
99
 
45
100
 
46
101
  class Solver(ABC):
102
+ def __init__(self, model, log_to_console):
103
+ self._model = model
104
+ self.solver_model: Optional[Any] = None
105
+ self.log_to_console = log_to_console
106
+
107
+ @abstractmethod
108
+ def create_solver_model(self) -> Any: ...
109
+
110
+ @abstractmethod
111
+ def set_attr(self, element, param_name, param_value): ...
112
+
113
+ @abstractmethod
114
+ def set_param(self, param_name, param_value): ...
115
+
116
+ @abstractmethod
117
+ def solve(self, log_fn, warmstart_fn, basis_fn, solution_file) -> Result: ...
118
+
119
+ @abstractmethod
120
+ def process_result(self, results: Result) -> Result: ...
121
+
122
+ def load_rc(self):
123
+ rc = self._get_all_rc()
124
+ for variable in self._model.variables:
125
+ variable.RC = rc
126
+
127
+ def load_slack(self):
128
+ slack = self._get_all_slack()
129
+ for constraint in self._model.constraints:
130
+ constraint.slack = slack
131
+
132
+ @abstractmethod
133
+ def _get_all_rc(self): ...
134
+
47
135
  @abstractmethod
48
- def solve(self, model, directory: Optional[Path] = None, **kwargs) -> Result: ...
136
+ def _get_all_slack(self): ...
49
137
 
50
138
 
51
139
  class FileBasedSolver(Solver):
52
- def solve(
53
- self,
54
- model: "Model",
55
- directory: Optional[Union[Path, str]] = None,
56
- use_var_names=None,
57
- **kwargs,
58
- ) -> Result:
140
+ def create_solver_model(
141
+ self, directory: Optional[Union[Path, str]], use_var_names, env
142
+ ) -> Any:
59
143
  problem_file = None
60
144
  if directory is not None:
61
145
  if isinstance(directory, str):
62
146
  directory = Path(directory)
63
147
  if not directory.exists():
64
148
  directory.mkdir(parents=True)
65
- filename = model.name if model.name is not None else "pyoframe-problem"
149
+ filename = (
150
+ self._model.name if self._model.name is not None else "pyoframe-problem"
151
+ )
66
152
  problem_file = directory / f"{filename}.lp"
67
- problem_file = model.to_file(problem_file, use_var_names=use_var_names)
68
- assert model.io_mappers is not None
153
+ problem_file = self._model.to_file(problem_file, use_var_names=use_var_names)
154
+ assert self._model.io_mappers is not None
155
+ return self.create_solver_model_from_lp(problem_file, env)
156
+
157
+ @abstractmethod
158
+ def create_solver_model_from_lp(self, problem_file: Path, env) -> Any: ...
69
159
 
70
- results = self.solve_from_lp(problem_file, **kwargs)
160
+ def set_attr(self, element, param_name, param_value):
161
+ if isinstance(param_value, pl.DataFrame):
162
+ if isinstance(element, pf.Variable):
163
+ param_value = self._model.io_mappers.var_map.apply(param_value)
164
+ elif isinstance(element, pf.Constraint):
165
+ param_value = self._model.io_mappers.const_map.apply(param_value)
166
+ return self.set_attr_unmapped(element, param_name, param_value)
71
167
 
168
+ @abstractmethod
169
+ def set_attr_unmapped(self, element, param_name, param_value): ...
170
+
171
+ def process_result(self, results: Result) -> Result:
72
172
  if results.solution is not None:
73
- results.solution.primal = model.io_mappers.var_map.undo(
173
+ results.solution.primal = self._model.io_mappers.var_map.undo(
74
174
  results.solution.primal
75
175
  )
76
176
  if results.solution.dual is not None:
77
- results.solution.dual = model.io_mappers.const_map.undo(
177
+ results.solution.dual = self._model.io_mappers.const_map.undo(
78
178
  results.solution.dual
79
179
  )
80
180
 
81
181
  return results
82
182
 
183
+ def _get_all_rc(self):
184
+ return self._model.io_mappers.var_map.undo(self._get_all_rc_unmapped())
185
+
186
+ def _get_all_slack(self):
187
+ return self._model.io_mappers.const_map.undo(self._get_all_slack_unmapped())
188
+
189
+ @abstractmethod
190
+ def _get_all_rc_unmapped(self): ...
191
+
83
192
  @abstractmethod
84
- def solve_from_lp(self, problem_file: Path, **kwargs) -> Result: ...
193
+ def _get_all_slack_unmapped(self): ...
85
194
 
86
195
 
196
+ @_register_solver("gurobi")
87
197
  class GurobiSolver(FileBasedSolver):
88
- def solve_from_lp(
89
- self,
90
- problem_fn,
91
- log_fn=None,
92
- warmstart_fn=None,
93
- basis_fn=None,
94
- solution_file=None,
95
- env=None,
96
- **solver_options,
97
- ) -> Result:
198
+ # see https://www.gurobi.com/documentation/10.0/refman/optimization_status_codes.html
199
+ CONDITION_MAP = {
200
+ 1: "unknown",
201
+ 2: "optimal",
202
+ 3: "infeasible",
203
+ 4: "infeasible_or_unbounded",
204
+ 5: "unbounded",
205
+ 6: "other",
206
+ 7: "iteration_limit",
207
+ 8: "terminated_by_limit",
208
+ 9: "time_limit",
209
+ 10: "optimal",
210
+ 11: "user_interrupt",
211
+ 12: "other",
212
+ 13: "suboptimal",
213
+ 14: "unknown",
214
+ 15: "terminated_by_limit",
215
+ 16: "internal_solver_error",
216
+ 17: "internal_solver_error",
217
+ }
218
+
219
+ def create_solver_model_from_lp(self, problem_fn, env) -> Result:
98
220
  """
99
221
  Solve a linear problem using the gurobi solver.
100
222
 
101
223
  This function communicates with gurobi using the gurubipy package.
102
224
  """
103
- import gurobipy
104
-
105
- # see https://www.gurobi.com/documentation/10.0/refman/optimization_status_codes.html
106
- CONDITION_MAP = {
107
- 1: "unknown",
108
- 2: "optimal",
109
- 3: "infeasible",
110
- 4: "infeasible_or_unbounded",
111
- 5: "unbounded",
112
- 6: "other",
113
- 7: "iteration_limit",
114
- 8: "terminated_by_limit",
115
- 9: "time_limit",
116
- 10: "optimal",
117
- 11: "user_interrupt",
118
- 12: "other",
119
- 13: "suboptimal",
120
- 14: "unknown",
121
- 15: "terminated_by_limit",
122
- 16: "internal_solver_error",
123
- 17: "internal_solver_error",
124
- }
125
-
126
- with contextlib.ExitStack() as stack:
127
- if env is None:
128
- env = stack.enter_context(gurobipy.Env())
129
-
130
- m = gurobipy.read(path_to_str(problem_fn), env=env)
131
- if solver_options is not None:
132
- for key, value in solver_options.items():
133
- m.setParam(key, value)
134
- if log_fn is not None:
135
- m.setParam("logfile", path_to_str(log_fn))
136
- if warmstart_fn:
137
- m.read(path_to_str(warmstart_fn))
138
-
139
- m.optimize()
140
-
141
- if basis_fn:
142
- try:
143
- m.write(path_to_str(basis_fn))
144
- except gurobipy.GurobiError as err:
145
- print("No model basis stored. Raised error: %s", err)
146
-
147
- condition = m.status
148
- termination_condition = CONDITION_MAP.get(condition, condition)
149
- status = Status.from_termination_condition(termination_condition)
150
-
151
- if status.is_ok:
152
- if solution_file:
153
- m.write(path_to_str(solution_file))
154
-
155
- objective = m.ObjVal
156
- vars = m.getVars()
157
- sol = pl.DataFrame(
158
- {
159
- NAME_COL: m.getAttr("VarName", vars),
160
- SOLUTION_KEY: m.getAttr("X", vars),
161
- }
162
- )
163
225
 
164
- constraints = m.getConstrs()
165
- try:
166
- dual = pl.DataFrame(
167
- {
168
- DUAL_KEY: m.getAttr("Pi", constraints),
169
- NAME_COL: m.getAttr("ConstrName", constraints),
170
- }
171
- )
172
- except gurobipy.GurobiError:
173
- dual = None
174
-
175
- solution = Solution(sol, dual, objective)
226
+ if env is None:
227
+ if self.log_to_console:
228
+ env = gurobipy.Env()
176
229
  else:
177
- solution = None
178
-
179
- return Result(status, solution, m)
180
-
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()
234
+
235
+ m = gurobipy.read(_path_to_str(problem_fn), env=env)
236
+ return m
237
+
238
+ def set_param(self, param_name, param_value):
239
+ self.solver_model.setParam(param_name, param_value)
240
+
241
+ @lru_cache
242
+ def _get_var_mapping(self):
243
+ vars = self.solver_model.getVars()
244
+ return vars, pl.DataFrame(
245
+ {VAR_KEY: self.solver_model.getAttr("VarName", vars)}
246
+ ).with_columns(i=pl.int_range(pl.len()))
247
+
248
+ @lru_cache
249
+ def _get_constraint_mapping(self):
250
+ constraints = self.solver_model.getConstrs()
251
+ return constraints, pl.DataFrame(
252
+ {CONSTRAINT_KEY: self.solver_model.getAttr("ConstrName", constraints)}
253
+ ).with_columns(i=pl.int_range(pl.len()))
254
+
255
+ def set_attr_unmapped(self, element, param_name, param_value):
256
+ if isinstance(element, pf.Model):
257
+ self.solver_model.setAttr(param_name, param_value)
258
+ elif isinstance(element, pf.Variable):
259
+ v, v_map = self._get_var_mapping()
260
+ param_value = param_value.join(v_map, on=VAR_KEY, how="left").drop(VAR_KEY)
261
+ self.solver_model.setAttr(
262
+ param_name,
263
+ [v[i] for i in param_value["i"]],
264
+ param_value[param_name],
265
+ )
266
+ elif isinstance(element, pf.Constraint):
267
+ c, c_map = self._get_constraint_mapping()
268
+ param_value = param_value.join(c_map, on=CONSTRAINT_KEY, how="left").drop(
269
+ CONSTRAINT_KEY
270
+ )
271
+ self.solver_model.setAttr(
272
+ param_name,
273
+ [c[i] for i in param_value["i"]],
274
+ param_value[param_name],
275
+ )
276
+ else:
277
+ raise ValueError(f"Element type {type(element)} not recognized.")
278
+
279
+ def solve(self, log_fn, warmstart_fn, basis_fn, solution_file) -> Result:
280
+ m = self.solver_model
281
+ if log_fn is not None:
282
+ m.setParam("logfile", _path_to_str(log_fn))
283
+ if warmstart_fn:
284
+ m.read(_path_to_str(warmstart_fn))
285
+
286
+ m.optimize()
287
+
288
+ if basis_fn:
289
+ try:
290
+ m.write(_path_to_str(basis_fn))
291
+ except gurobipy.GurobiError as err:
292
+ print("No model basis stored. Raised error: %s", err)
293
+
294
+ condition = m.status
295
+ termination_condition = GurobiSolver.CONDITION_MAP.get(condition, condition)
296
+ status = Status.from_termination_condition(termination_condition)
297
+
298
+ if status.is_ok:
299
+ if solution_file:
300
+ m.write(_path_to_str(solution_file))
301
+
302
+ objective = m.ObjVal
303
+ vars = m.getVars()
304
+ sol = pl.DataFrame(
305
+ {
306
+ VAR_KEY: m.getAttr("VarName", vars),
307
+ SOLUTION_KEY: m.getAttr("X", vars),
308
+ }
309
+ )
181
310
 
182
- def path_to_str(path: Union[Path, str]) -> str:
311
+ constraints = m.getConstrs()
312
+ try:
313
+ dual = pl.DataFrame(
314
+ {
315
+ DUAL_KEY: m.getAttr("Pi", constraints),
316
+ CONSTRAINT_KEY: m.getAttr("ConstrName", constraints),
317
+ }
318
+ )
319
+ except gurobipy.GurobiError:
320
+ dual = None
321
+
322
+ solution = Solution(sol, dual, objective)
323
+ else:
324
+ solution = None
325
+
326
+ return Result(status, solution)
327
+
328
+ def _get_all_rc_unmapped(self):
329
+ m = self._model.solver_model
330
+ vars = m.getVars()
331
+ return pl.DataFrame(
332
+ {
333
+ RC_COL: m.getAttr("RC", vars),
334
+ VAR_KEY: m.getAttr("VarName", vars),
335
+ }
336
+ )
337
+
338
+ def _get_all_slack_unmapped(self):
339
+ m = self._model.solver_model
340
+ constraints = m.getConstrs()
341
+ return pl.DataFrame(
342
+ {
343
+ SLACK_COL: m.getAttr("Slack", constraints),
344
+ CONSTRAINT_KEY: m.getAttr("ConstrName", constraints),
345
+ }
346
+ )
347
+
348
+
349
+ def _path_to_str(path: Union[Path, str]) -> str:
183
350
  """
184
351
  Convert a pathlib.Path to a string.
185
352
  """
@@ -0,0 +1,60 @@
1
+ """
2
+ Contains the base classes to support .params and .attr containers for user-defined parameters and attributes.
3
+ """
4
+
5
+ from typing import Any
6
+
7
+
8
+ class Container:
9
+ """
10
+ A container for user-defined attributes or parameters.
11
+
12
+ Parameters:
13
+ preprocess : Callable[str, Any], optional
14
+ A function to preprocess user-defined values before adding them to the container.
15
+
16
+ Examples:
17
+ >>> params = Container()
18
+ >>> params.a = 1
19
+ >>> params.b = 2
20
+ >>> params.a
21
+ 1
22
+ >>> params.b
23
+ 2
24
+ >>> for k, v in params:
25
+ ... print(k, v)
26
+ a 1
27
+ b 2
28
+ """
29
+
30
+ def __init__(self, preprocess=None):
31
+ self._preprocess = preprocess
32
+ self._attributes = {}
33
+
34
+ def __setattr__(self, name: str, value: Any) -> None:
35
+ if name.startswith("_"):
36
+ return super().__setattr__(name, value)
37
+ if self._preprocess is not None:
38
+ value = self._preprocess(name, value)
39
+ self._attributes[name] = value
40
+
41
+ def __getattr__(self, name: str) -> Any:
42
+ if name.startswith("_"):
43
+ return super().__getattribute__(name)
44
+ return self._attributes[name]
45
+
46
+ def __iter__(self):
47
+ return iter(self._attributes.items())
48
+
49
+
50
+ class AttrContainerMixin:
51
+ def __init__(self, *args, **kwargs) -> None:
52
+ super().__init__(*args, **kwargs)
53
+ self.attr = Container(preprocess=self._preprocess_attr)
54
+
55
+ def _preprocess_attr(self, name: str, value: Any) -> Any:
56
+ """
57
+ Preprocesses user-defined values before adding them to the Params container.
58
+ By default this function does nothing but subclasses can override it.
59
+ """
60
+ return value
pyoframe/util.py CHANGED
@@ -2,68 +2,17 @@
2
2
  File containing utility functions and classes.
3
3
  """
4
4
 
5
- from abc import abstractmethod, ABC
6
- from collections import defaultdict
7
- from typing import Any, Dict, Iterable, Optional, Union
5
+ from typing import Any, Iterable, Optional, Union, List, Dict
6
+
7
+ from dataclasses import dataclass, field
8
8
 
9
9
  import polars as pl
10
10
  import pandas as pd
11
+ from functools import wraps
11
12
 
12
13
  from pyoframe.constants import COEF_KEY, CONST_TERM, RESERVED_COL_KEYS, VAR_KEY
13
14
 
14
15
 
15
- class IdCounterMixin(ABC):
16
- """
17
- Provides a method that assigns a unique ID to each row in a DataFrame.
18
- IDs start at 1 and go up consecutively. No zero ID is assigned since it is reserved for the constant variable term.
19
- IDs are only unique for the subclass since different subclasses have different counters.
20
- """
21
-
22
- # Keys are the subclass names and values are the next unasigned ID.
23
- _id_counters: Dict[str, int] = defaultdict(lambda: 1)
24
-
25
- @classmethod
26
- def _reset_counters(cls):
27
- """
28
- Resets all the ID counters.
29
- This function is called before every unit test to reset the code state.
30
- """
31
- cls._id_counters = defaultdict(lambda: 1)
32
-
33
- def _assign_ids(self, df: pl.DataFrame) -> pl.DataFrame:
34
- """
35
- Adds the column `to_column` to the DataFrame `df` with the next batch
36
- of unique consecutive IDs.
37
- """
38
- cls_name = self.__class__.__name__
39
- cur_count = self._id_counters[cls_name]
40
- id_col_name = self.get_id_column_name()
41
-
42
- if df.height == 0:
43
- df = df.with_columns(pl.lit(cur_count).alias(id_col_name))
44
- else:
45
- df = df.with_columns(
46
- pl.int_range(cur_count, cur_count + pl.len()).alias(id_col_name)
47
- )
48
- df = df.with_columns(pl.col(id_col_name).cast(pl.UInt32))
49
- self._id_counters[cls_name] += df.height
50
- return df
51
-
52
- @classmethod
53
- @abstractmethod
54
- def get_id_column_name(cls) -> str:
55
- """
56
- Returns the name of the column containing the IDs.
57
- """
58
-
59
- @property
60
- @abstractmethod
61
- def ids(self) -> pl.DataFrame:
62
- """
63
- Returns a dataframe with the IDs and any other relevant columns (i.e. the dimension columns).
64
- """
65
-
66
-
67
16
  def get_obj_repr(obj: object, _props: Iterable[str] = (), **kwargs):
68
17
  """
69
18
  Helper function to generate __repr__ strings for classes. See usage for examples.
@@ -269,3 +218,55 @@ def cast_coef_to_string(
269
218
  return df.with_columns(pl.concat_str("_sign", column_name).alias(column_name)).drop(
270
219
  "_sign"
271
220
  )
221
+
222
+
223
+ def unwrap_single_values(func):
224
+ """Decorator for functions that return DataFrames. Returned dataframes with a single value will instead return the value."""
225
+
226
+ @wraps(func)
227
+ def wrapper(*args, **kwargs):
228
+ result = func(*args, **kwargs)
229
+ if isinstance(result, pl.DataFrame) and result.shape == (1, 1):
230
+ return result.item()
231
+ return result
232
+
233
+ return wrapper
234
+
235
+
236
+ def dataframe_to_tupled_list(
237
+ df: pl.DataFrame, num_max_elements: Optional[int] = None
238
+ ) -> str:
239
+ """
240
+ Converts a dataframe into a list of tuples. Used to print a Set to the console. See examples for behaviour.
241
+
242
+ Examples:
243
+ >>> df = pl.DataFrame({"x": [1, 2, 3, 4, 5]})
244
+ >>> dataframe_to_tupled_list(df)
245
+ '[1, 2, 3, 4, 5]'
246
+ >>> dataframe_to_tupled_list(df, 3)
247
+ '[1, 2, 3, ...]'
248
+
249
+ >>> df = pl.DataFrame({"x": [1, 2, 3, 4, 5], "y": [2, 3, 4, 5, 6]})
250
+ >>> dataframe_to_tupled_list(df, 3)
251
+ '[(1, 2), (2, 3), (3, 4), ...]'
252
+ """
253
+ elipse = False
254
+ if num_max_elements is not None:
255
+ if len(df) > num_max_elements:
256
+ elipse = True
257
+ df = df.head(num_max_elements)
258
+
259
+ res = (row for row in df.iter_rows())
260
+ if len(df.columns) == 1:
261
+ res = (row[0] for row in res)
262
+
263
+ res = str(list(res))
264
+ if elipse:
265
+ res = res[:-1] + ", ...]"
266
+ return res
267
+
268
+
269
+ @dataclass
270
+ class FuncArgs:
271
+ args: List
272
+ kwargs: Dict = field(default_factory=dict)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyoframe
3
- Version: 0.0.4
3
+ Version: 0.0.5
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/
@@ -15,10 +15,11 @@ Classifier: Natural Language :: English
15
15
  Requires-Python: >=3.8
16
16
  Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
- Requires-Dist: polars ==0.20.13
18
+ Requires-Dist: polars
19
19
  Requires-Dist: numpy
20
20
  Requires-Dist: pyarrow
21
21
  Requires-Dist: pandas
22
+ Requires-Dist: tqdm
22
23
  Provides-Extra: dev
23
24
  Requires-Dist: black ; extra == 'dev'
24
25
  Requires-Dist: bumpver ; extra == 'dev'
@@ -56,3 +57,9 @@ Contributions are welcome! See [`CONTRIBUTE.md`](./CONTRIBUTE.md).
56
57
  ## Acknowledgments
57
58
 
58
59
  Martin Staadecker first created this library while working for [Bravos Power](https://www.bravospower.com/) The library takes inspiration from Linopy and Pyomo, two prior libraries for optimization for which we are thankful.
60
+
61
+ ## Troubleshooting Common Errors
62
+
63
+ ### `datatypes of join keys don't match`
64
+
65
+ Often, this error indicates that two dataframes in your inputs representing the same dimension have different datatypes (e.g. 16bit integer and 64bit integer). This is not allowed and you should ensure for the same dimensions, datatypes are identical.
@@ -0,0 +1,18 @@
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,,