pyoframe 0.1.4__py3-none-any.whl → 0.2.0__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/__init__.py +5 -0
- pyoframe/_arithmetic.py +13 -14
- pyoframe/_version.py +21 -0
- pyoframe/constants.py +12 -7
- pyoframe/core.py +169 -72
- pyoframe/model.py +63 -35
- pyoframe/model_element.py +3 -4
- pyoframe/util.py +3 -3
- {pyoframe-0.1.4.dist-info → pyoframe-0.2.0.dist-info}/METADATA +19 -18
- pyoframe-0.2.0.dist-info/RECORD +15 -0
- {pyoframe-0.1.4.dist-info → pyoframe-0.2.0.dist-info}/WHEEL +1 -1
- {pyoframe-0.1.4.dist-info → pyoframe-0.2.0.dist-info/licenses}/LICENSE +1 -1
- pyoframe-0.1.4.dist-info/RECORD +0 -14
- {pyoframe-0.1.4.dist-info → pyoframe-0.2.0.dist-info}/top_level.txt +0 -0
pyoframe/__init__.py
CHANGED
|
@@ -8,6 +8,11 @@ from pyoframe.core import Constraint, Expression, Set, Variable, sum, sum_by
|
|
|
8
8
|
from pyoframe.model import Model
|
|
9
9
|
from pyoframe.monkey_patch import patch_dataframe_libraries
|
|
10
10
|
|
|
11
|
+
try:
|
|
12
|
+
from pyoframe._version import __version__, __version_tuple__ # noqa: F401
|
|
13
|
+
except ModuleNotFoundError: # pragma: no cover
|
|
14
|
+
pass
|
|
15
|
+
|
|
11
16
|
patch_dataframe_libraries()
|
|
12
17
|
|
|
13
18
|
__all__ = [
|
pyoframe/_arithmetic.py
CHANGED
|
@@ -10,7 +10,6 @@ from pyoframe.constants import (
|
|
|
10
10
|
COEF_KEY,
|
|
11
11
|
CONST_TERM,
|
|
12
12
|
KEY_TYPE,
|
|
13
|
-
POLARS_VERSION,
|
|
14
13
|
QUAD_VAR_KEY,
|
|
15
14
|
RESERVED_COL_KEYS,
|
|
16
15
|
VAR_KEY,
|
|
@@ -81,14 +80,14 @@ def _multiply_expressions_core(self: "Expression", other: "Expression") -> "Expr
|
|
|
81
80
|
self, other = other, self
|
|
82
81
|
self_degree, other_degree = other_degree, self_degree
|
|
83
82
|
if other_degree == 1:
|
|
84
|
-
assert (
|
|
85
|
-
|
|
86
|
-
)
|
|
83
|
+
assert self_degree == 1, (
|
|
84
|
+
"This should always be true since the sum of degrees must be <=2."
|
|
85
|
+
)
|
|
87
86
|
return _quadratic_multiplication(self, other)
|
|
88
87
|
|
|
89
|
-
assert (
|
|
90
|
-
|
|
91
|
-
)
|
|
88
|
+
assert other_degree == 0, (
|
|
89
|
+
"This should always be true since other cases have already been handled."
|
|
90
|
+
)
|
|
92
91
|
multiplier = other.data.drop(
|
|
93
92
|
VAR_KEY
|
|
94
93
|
) # QUAD_VAR_KEY doesn't need to be dropped since we know it doesn't exist
|
|
@@ -248,12 +247,12 @@ def _add_expressions_core(*expressions: "Expression") -> "Expression":
|
|
|
248
247
|
left_data = left.data.join(get_indices(right), how="inner", on=dims)
|
|
249
248
|
right_data = right.data.join(get_indices(left), how="inner", on=dims)
|
|
250
249
|
elif strat == (UnmatchedStrategy.UNSET, UnmatchedStrategy.UNSET):
|
|
251
|
-
assert (
|
|
252
|
-
not
|
|
253
|
-
)
|
|
250
|
+
assert not Config.disable_unmatched_checks, (
|
|
251
|
+
"This code should not be reached when unmatched checks are disabled."
|
|
252
|
+
)
|
|
254
253
|
outer_join = get_indices(left).join(
|
|
255
254
|
get_indices(right),
|
|
256
|
-
how="full"
|
|
255
|
+
how="full",
|
|
257
256
|
on=dims,
|
|
258
257
|
)
|
|
259
258
|
if outer_join.get_column(dims[0]).null_count() > 0:
|
|
@@ -280,9 +279,9 @@ def _add_expressions_core(*expressions: "Expression") -> "Expression":
|
|
|
280
279
|
+ str(left_data.filter(left_data.get_column(COEF_KEY).is_null()))
|
|
281
280
|
)
|
|
282
281
|
elif strat == (UnmatchedStrategy.KEEP, UnmatchedStrategy.UNSET):
|
|
283
|
-
assert (
|
|
284
|
-
not
|
|
285
|
-
)
|
|
282
|
+
assert not Config.disable_unmatched_checks, (
|
|
283
|
+
"This code should not be reached when unmatched checks are disabled."
|
|
284
|
+
)
|
|
286
285
|
unmatched = right.data.join(get_indices(left), how="anti", on=dims)
|
|
287
286
|
if len(unmatched) > 0:
|
|
288
287
|
raise PyoframeError(
|
pyoframe/_version.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
|
|
5
|
+
|
|
6
|
+
TYPE_CHECKING = False
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from typing import Tuple
|
|
9
|
+
from typing import Union
|
|
10
|
+
|
|
11
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
12
|
+
else:
|
|
13
|
+
VERSION_TUPLE = object
|
|
14
|
+
|
|
15
|
+
version: str
|
|
16
|
+
__version__: str
|
|
17
|
+
__version_tuple__: VERSION_TUPLE
|
|
18
|
+
version_tuple: VERSION_TUPLE
|
|
19
|
+
|
|
20
|
+
__version__ = version = '0.2.0'
|
|
21
|
+
__version_tuple__ = version_tuple = (0, 2, 0)
|
pyoframe/constants.py
CHANGED
|
@@ -8,11 +8,6 @@ from typing import Literal, Optional
|
|
|
8
8
|
|
|
9
9
|
import polars as pl
|
|
10
10
|
import pyoptinterface as poi
|
|
11
|
-
from packaging import version
|
|
12
|
-
|
|
13
|
-
# Constant to help split our logic depending on the polars version in use.
|
|
14
|
-
# This approach is compatible with polars-lts-cpu.
|
|
15
|
-
POLARS_VERSION = version.parse(pl.__version__)
|
|
16
11
|
|
|
17
12
|
COEF_KEY = "__coeff"
|
|
18
13
|
VAR_KEY = "__variable_id"
|
|
@@ -20,6 +15,7 @@ QUAD_VAR_KEY = "__quadratic_variable_id"
|
|
|
20
15
|
CONSTRAINT_KEY = "__constraint_id"
|
|
21
16
|
SOLUTION_KEY = "solution"
|
|
22
17
|
DUAL_KEY = "dual"
|
|
18
|
+
SUPPORTED_SOLVERS = ["gurobi", "highs"]
|
|
23
19
|
SUPPORTED_SOLVER_TYPES = Literal["gurobi", "highs"]
|
|
24
20
|
KEY_TYPE = pl.UInt32
|
|
25
21
|
|
|
@@ -44,7 +40,7 @@ class _ConfigMeta(type):
|
|
|
44
40
|
cls._defaults = {
|
|
45
41
|
k: v
|
|
46
42
|
for k, v in dct.items()
|
|
47
|
-
if not k.startswith("_") and type(v) != classmethod
|
|
43
|
+
if not k.startswith("_") and type(v) != classmethod # noqa: E721 (didn't want to mess with it since it works)
|
|
48
44
|
}
|
|
49
45
|
|
|
50
46
|
|
|
@@ -59,10 +55,19 @@ class Config(metaclass=_ConfigMeta):
|
|
|
59
55
|
print_uses_variable_names: bool = True
|
|
60
56
|
print_max_line_length: int = 80
|
|
61
57
|
print_max_lines: int = 15
|
|
62
|
-
# Number of elements to show when printing a set to the console (additional elements are replaced with ...)
|
|
63
58
|
print_max_set_elements: int = 50
|
|
59
|
+
"Number of elements to show when printing a set to the console (additional elements are replaced with ...)"
|
|
60
|
+
|
|
64
61
|
enable_is_duplicated_expression_safety_check: bool = False
|
|
65
62
|
|
|
63
|
+
integer_tolerance: float = 1e-8
|
|
64
|
+
"""
|
|
65
|
+
For convenience, Pyoframe returns the solution of integer and binary variables as integers not floating point values.
|
|
66
|
+
To do so, Pyoframe must convert the solver-provided floating point values to integers. To avoid unexpected rounding errors,
|
|
67
|
+
Pyoframe uses this tolerance to check that the floating point result is an integer as expected. Overly tight tolerances can trigger
|
|
68
|
+
unexpected errors. Setting the tolerance to zero disables the check.
|
|
69
|
+
"""
|
|
70
|
+
|
|
66
71
|
@classmethod
|
|
67
72
|
def reset_defaults(cls):
|
|
68
73
|
"""
|
pyoframe/core.py
CHANGED
|
@@ -33,7 +33,6 @@ from pyoframe.constants import (
|
|
|
33
33
|
CONSTRAINT_KEY,
|
|
34
34
|
DUAL_KEY,
|
|
35
35
|
KEY_TYPE,
|
|
36
|
-
POLARS_VERSION,
|
|
37
36
|
QUAD_VAR_KEY,
|
|
38
37
|
RESERVED_COL_KEYS,
|
|
39
38
|
SOLUTION_KEY,
|
|
@@ -294,9 +293,9 @@ class Set(ModelElement, SupportsMath, SupportPolarsMethodMixin):
|
|
|
294
293
|
over_merged = over_frames[0]
|
|
295
294
|
|
|
296
295
|
for df in over_frames[1:]:
|
|
297
|
-
assert (
|
|
298
|
-
|
|
299
|
-
)
|
|
296
|
+
assert set(over_merged.columns) & set(df.columns) == set(), (
|
|
297
|
+
"All coordinates must have unique column names."
|
|
298
|
+
)
|
|
300
299
|
over_merged = over_merged.join(df, how="cross")
|
|
301
300
|
return over_merged
|
|
302
301
|
|
|
@@ -309,9 +308,9 @@ class Set(ModelElement, SupportsMath, SupportPolarsMethodMixin):
|
|
|
309
308
|
|
|
310
309
|
def __mul__(self, other):
|
|
311
310
|
if isinstance(other, Set):
|
|
312
|
-
assert (
|
|
313
|
-
|
|
314
|
-
)
|
|
311
|
+
assert set(self.data.columns) & set(other.data.columns) == set(), (
|
|
312
|
+
"Cannot multiply two sets with columns in common."
|
|
313
|
+
)
|
|
315
314
|
return Set(self.data, other.data)
|
|
316
315
|
return super().__mul__(other)
|
|
317
316
|
|
|
@@ -346,18 +345,11 @@ class Set(ModelElement, SupportsMath, SupportPolarsMethodMixin):
|
|
|
346
345
|
elif isinstance(set, Constraint):
|
|
347
346
|
df = set.data.select(set.dimensions_unsafe)
|
|
348
347
|
elif isinstance(set, SupportsMath):
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
)
|
|
355
|
-
else:
|
|
356
|
-
df = (
|
|
357
|
-
set.to_expr()
|
|
358
|
-
.data.drop(RESERVED_COL_KEYS, strict=False)
|
|
359
|
-
.unique(maintain_order=True)
|
|
360
|
-
)
|
|
348
|
+
df = (
|
|
349
|
+
set.to_expr()
|
|
350
|
+
.data.drop(RESERVED_COL_KEYS, strict=False)
|
|
351
|
+
.unique(maintain_order=True)
|
|
352
|
+
)
|
|
361
353
|
elif isinstance(set, pd.Index):
|
|
362
354
|
df = pl.from_pandas(pd.DataFrame(index=set).reset_index())
|
|
363
355
|
elif isinstance(set, pd.DataFrame):
|
|
@@ -620,13 +612,13 @@ class Expression(ModelElement, SupportsMath, SupportPolarsMethodMixin):
|
|
|
620
612
|
"""
|
|
621
613
|
df: pl.DataFrame = Set(set).data
|
|
622
614
|
set_dims = _get_dimensions(df)
|
|
623
|
-
assert (
|
|
624
|
-
|
|
625
|
-
)
|
|
615
|
+
assert set_dims is not None, (
|
|
616
|
+
"Cannot use .within() with a set with no dimensions."
|
|
617
|
+
)
|
|
626
618
|
dims = self.dimensions
|
|
627
|
-
assert (
|
|
628
|
-
|
|
629
|
-
)
|
|
619
|
+
assert dims is not None, (
|
|
620
|
+
"Cannot use .within() with an expression with no dimensions."
|
|
621
|
+
)
|
|
630
622
|
dims_in_common = [dim for dim in dims if dim in set_dims]
|
|
631
623
|
by_dims = df.select(dims_in_common).unique(maintain_order=True)
|
|
632
624
|
return self._new(self.data.join(by_dims, on=dims_in_common))
|
|
@@ -800,15 +792,9 @@ class Expression(ModelElement, SupportsMath, SupportPolarsMethodMixin):
|
|
|
800
792
|
keys = keys.with_columns(
|
|
801
793
|
pl.lit(CONST_TERM).alias(QUAD_VAR_KEY).cast(KEY_TYPE)
|
|
802
794
|
)
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
)
|
|
807
|
-
else:
|
|
808
|
-
data = data.join(
|
|
809
|
-
keys, on=dim + self._variable_columns, how="outer_coalesce"
|
|
810
|
-
)
|
|
811
|
-
data = data.with_columns(pl.col(COEF_KEY).fill_null(0.0))
|
|
795
|
+
data = data.join(
|
|
796
|
+
keys, on=dim + self._variable_columns, how="full", coalesce=True
|
|
797
|
+
).with_columns(pl.col(COEF_KEY).fill_null(0.0))
|
|
812
798
|
|
|
813
799
|
data = data.with_columns(
|
|
814
800
|
pl.when(pl.col(VAR_KEY) == CONST_TERM)
|
|
@@ -826,10 +812,7 @@ class Expression(ModelElement, SupportsMath, SupportPolarsMethodMixin):
|
|
|
826
812
|
constant_terms = constant_terms.drop(QUAD_VAR_KEY)
|
|
827
813
|
if dims is not None:
|
|
828
814
|
dims_df = self.data.select(dims).unique(maintain_order=True)
|
|
829
|
-
|
|
830
|
-
df = constant_terms.join(dims_df, on=dims, how="full", coalesce=True)
|
|
831
|
-
else:
|
|
832
|
-
df = constant_terms.join(dims_df, on=dims, how="outer_coalesce")
|
|
815
|
+
df = constant_terms.join(dims_df, on=dims, how="full", coalesce=True)
|
|
833
816
|
return df.with_columns(pl.col(COEF_KEY).fill_null(0.0))
|
|
834
817
|
else:
|
|
835
818
|
if len(constant_terms) == 0:
|
|
@@ -870,9 +853,9 @@ class Expression(ModelElement, SupportsMath, SupportPolarsMethodMixin):
|
|
|
870
853
|
>>> m.expr_2.evaluate()
|
|
871
854
|
63.0
|
|
872
855
|
"""
|
|
873
|
-
assert (
|
|
874
|
-
|
|
875
|
-
)
|
|
856
|
+
assert self._model is not None, (
|
|
857
|
+
"Expression must be added to the model to use .value"
|
|
858
|
+
)
|
|
876
859
|
|
|
877
860
|
df = self.data
|
|
878
861
|
sm = self._model.poi
|
|
@@ -899,16 +882,23 @@ class Expression(ModelElement, SupportsMath, SupportPolarsMethodMixin):
|
|
|
899
882
|
df = df.group_by(dims, maintain_order=True)
|
|
900
883
|
return df.sum()
|
|
901
884
|
|
|
902
|
-
def to_poi(self) -> poi.ScalarAffineFunction:
|
|
885
|
+
def to_poi(self) -> poi.ScalarAffineFunction | poi.ScalarQuadraticFunction:
|
|
903
886
|
if self.dimensions is not None:
|
|
904
887
|
raise ValueError(
|
|
905
888
|
"Only non-dimensioned expressions can be converted to PyOptInterface."
|
|
906
889
|
) # pragma: no cover
|
|
907
890
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
891
|
+
if self.is_quadratic:
|
|
892
|
+
return poi.ScalarQuadraticFunction(
|
|
893
|
+
coefficients=self.data.get_column(COEF_KEY).to_numpy(),
|
|
894
|
+
var1s=self.data.get_column(VAR_KEY).to_numpy(),
|
|
895
|
+
var2s=self.data.get_column(QUAD_VAR_KEY).to_numpy(),
|
|
896
|
+
)
|
|
897
|
+
else:
|
|
898
|
+
return poi.ScalarAffineFunction(
|
|
899
|
+
coefficients=self.data.get_column(COEF_KEY).to_numpy(),
|
|
900
|
+
variables=self.data.get_column(VAR_KEY).to_numpy(),
|
|
901
|
+
)
|
|
912
902
|
|
|
913
903
|
def to_str_table(self, include_const_term=True):
|
|
914
904
|
data = self.data if include_const_term else self.variable_terms
|
|
@@ -951,10 +941,10 @@ class Expression(ModelElement, SupportsMath, SupportPolarsMethodMixin):
|
|
|
951
941
|
|
|
952
942
|
if dimensions is not None:
|
|
953
943
|
data = data.group_by(dimensions, maintain_order=True).agg(
|
|
954
|
-
pl.col("expr").str.
|
|
944
|
+
pl.col("expr").str.join(delimiter=" ")
|
|
955
945
|
)
|
|
956
946
|
else:
|
|
957
|
-
data = data.select(pl.col("expr").str.
|
|
947
|
+
data = data.select(pl.col("expr").str.join(delimiter=" "))
|
|
958
948
|
|
|
959
949
|
# Remove leading +
|
|
960
950
|
data = data.with_columns(pl.col("expr").str.strip_chars(characters=" +"))
|
|
@@ -1010,7 +1000,7 @@ class Expression(ModelElement, SupportsMath, SupportPolarsMethodMixin):
|
|
|
1010
1000
|
include_const_term=include_const_term,
|
|
1011
1001
|
)
|
|
1012
1002
|
str_table = self.to_str_create_prefix(str_table)
|
|
1013
|
-
result += str_table.select(pl.col("expr").str.
|
|
1003
|
+
result += str_table.select(pl.col("expr").str.join(delimiter="\n")).item()
|
|
1014
1004
|
|
|
1015
1005
|
return result
|
|
1016
1006
|
|
|
@@ -1054,6 +1044,31 @@ def sum(
|
|
|
1054
1044
|
over: Union[str, Sequence[str], SupportsToExpr],
|
|
1055
1045
|
expr: Optional[SupportsToExpr] = None,
|
|
1056
1046
|
) -> "Expression":
|
|
1047
|
+
"""
|
|
1048
|
+
Sum an expression over specified dimensions.
|
|
1049
|
+
If no dimensions are specified, the sum is taken over all of the expression's dimensions.
|
|
1050
|
+
|
|
1051
|
+
Examples:
|
|
1052
|
+
>>> expr = pl.DataFrame({
|
|
1053
|
+
... "time": ["mon", "tue", "wed", "mon", "tue"],
|
|
1054
|
+
... "place": ["Toronto", "Toronto", "Toronto", "Vancouver", "Vancouver"],
|
|
1055
|
+
... "tiktok_posts": [1e6, 3e6, 2e6, 1e6, 2e6]
|
|
1056
|
+
... }).to_expr()
|
|
1057
|
+
>>> expr
|
|
1058
|
+
<Expression size=5 dimensions={'time': 3, 'place': 2} terms=5>
|
|
1059
|
+
[mon,Toronto]: 1000000
|
|
1060
|
+
[tue,Toronto]: 3000000
|
|
1061
|
+
[wed,Toronto]: 2000000
|
|
1062
|
+
[mon,Vancouver]: 1000000
|
|
1063
|
+
[tue,Vancouver]: 2000000
|
|
1064
|
+
>>> pf.sum("time", expr)
|
|
1065
|
+
<Expression size=2 dimensions={'place': 2} terms=2>
|
|
1066
|
+
[Toronto]: 6000000
|
|
1067
|
+
[Vancouver]: 3000000
|
|
1068
|
+
>>> pf.sum(expr)
|
|
1069
|
+
<Expression size=1 dimensions={} terms=1>
|
|
1070
|
+
9000000
|
|
1071
|
+
"""
|
|
1057
1072
|
if expr is None:
|
|
1058
1073
|
assert isinstance(over, SupportsMath)
|
|
1059
1074
|
over = over.to_expr()
|
|
@@ -1069,13 +1084,34 @@ def sum(
|
|
|
1069
1084
|
|
|
1070
1085
|
|
|
1071
1086
|
def sum_by(by: Union[str, Sequence[str]], expr: SupportsToExpr) -> "Expression":
|
|
1087
|
+
"""
|
|
1088
|
+
Like `pf.sum()`, but the sum is taken over all dimensions except those specified in `by` (just like a groupby operation).
|
|
1089
|
+
|
|
1090
|
+
Examples:
|
|
1091
|
+
>>> expr = pl.DataFrame({
|
|
1092
|
+
... "time": ["mon", "tue", "wed", "mon", "tue"],
|
|
1093
|
+
... "place": ["Toronto", "Toronto", "Toronto", "Vancouver", "Vancouver"],
|
|
1094
|
+
... "tiktok_posts": [1e6, 3e6, 2e6, 1e6, 2e6]
|
|
1095
|
+
... }).to_expr()
|
|
1096
|
+
>>> expr
|
|
1097
|
+
<Expression size=5 dimensions={'time': 3, 'place': 2} terms=5>
|
|
1098
|
+
[mon,Toronto]: 1000000
|
|
1099
|
+
[tue,Toronto]: 3000000
|
|
1100
|
+
[wed,Toronto]: 2000000
|
|
1101
|
+
[mon,Vancouver]: 1000000
|
|
1102
|
+
[tue,Vancouver]: 2000000
|
|
1103
|
+
>>> pf.sum_by("place", expr)
|
|
1104
|
+
<Expression size=2 dimensions={'place': 2} terms=2>
|
|
1105
|
+
[Toronto]: 6000000
|
|
1106
|
+
[Vancouver]: 3000000
|
|
1107
|
+
"""
|
|
1072
1108
|
if isinstance(by, str):
|
|
1073
1109
|
by = [by]
|
|
1074
1110
|
expr = expr.to_expr()
|
|
1075
1111
|
dimensions = expr.dimensions
|
|
1076
|
-
assert (
|
|
1077
|
-
dimensions
|
|
1078
|
-
)
|
|
1112
|
+
assert dimensions is not None, (
|
|
1113
|
+
"Cannot sum by dimensions with an expression with no dimensions."
|
|
1114
|
+
)
|
|
1079
1115
|
remaining_dims = [dim for dim in dimensions if dim not in by]
|
|
1080
1116
|
return sum(over=remaining_dims, expr=expr)
|
|
1081
1117
|
|
|
@@ -1329,9 +1365,9 @@ class Constraint(ModelElementWithId):
|
|
|
1329
1365
|
return self
|
|
1330
1366
|
|
|
1331
1367
|
var_name = f"{self.name}_relaxation"
|
|
1332
|
-
assert not hasattr(
|
|
1333
|
-
|
|
1334
|
-
)
|
|
1368
|
+
assert not hasattr(m, var_name), (
|
|
1369
|
+
"Conflicting names, relaxation variable already exists on the model."
|
|
1370
|
+
)
|
|
1335
1371
|
var = Variable(self, lb=0, ub=max)
|
|
1336
1372
|
setattr(m, var_name, var)
|
|
1337
1373
|
|
|
@@ -1374,7 +1410,7 @@ class Constraint(ModelElementWithId):
|
|
|
1374
1410
|
[str_table, rhs], how=("align" if dims else "horizontal")
|
|
1375
1411
|
)
|
|
1376
1412
|
constr_str = constr_str.select(
|
|
1377
|
-
pl.concat_str("expr", pl.lit(f" {self.sense.value} "), "rhs").str.
|
|
1413
|
+
pl.concat_str("expr", pl.lit(f" {self.sense.value} "), "rhs").str.join(
|
|
1378
1414
|
delimiter="\n"
|
|
1379
1415
|
)
|
|
1380
1416
|
).item()
|
|
@@ -1459,9 +1495,9 @@ class Variable(ModelElementWithId, SupportsMath, SupportPolarsMethodMixin):
|
|
|
1459
1495
|
equals: Optional[SupportsMath] = None,
|
|
1460
1496
|
):
|
|
1461
1497
|
if equals is not None:
|
|
1462
|
-
assert (
|
|
1463
|
-
|
|
1464
|
-
)
|
|
1498
|
+
assert len(indexing_sets) == 0, (
|
|
1499
|
+
"Cannot specify both 'equals' and 'indexing_sets'"
|
|
1500
|
+
)
|
|
1465
1501
|
indexing_sets = (equals,)
|
|
1466
1502
|
|
|
1467
1503
|
data = Set(*indexing_sets).data if len(indexing_sets) > 0 else pl.DataFrame()
|
|
@@ -1578,9 +1614,78 @@ class Variable(ModelElementWithId, SupportsMath, SupportPolarsMethodMixin):
|
|
|
1578
1614
|
@property
|
|
1579
1615
|
@unwrap_single_values
|
|
1580
1616
|
def solution(self):
|
|
1581
|
-
|
|
1617
|
+
"""
|
|
1618
|
+
Retrieve a variable's optimal value after the model has been solved.
|
|
1619
|
+
Returned as a DataFrame if the variable has dimensions, otherwise as a single value.
|
|
1620
|
+
Binary and integer variables are returned as integers.
|
|
1621
|
+
|
|
1622
|
+
Examples:
|
|
1623
|
+
>>> m = pf.Model()
|
|
1624
|
+
>>> m.var_continuous = pf.Variable({"dim1": [1, 2, 3]}, lb=5, ub=5)
|
|
1625
|
+
>>> m.var_integer = pf.Variable({"dim1": [1, 2, 3]}, lb=4.5, ub=5.5, vtype=VType.INTEGER)
|
|
1626
|
+
>>> m.var_dimensionless = pf.Variable(lb=4.5, ub=5.5, vtype=VType.INTEGER)
|
|
1627
|
+
>>> m.var_continuous.solution
|
|
1628
|
+
Traceback (most recent call last):
|
|
1629
|
+
...
|
|
1630
|
+
RuntimeError: Failed to retrieve solution for variable. Are you sure the model has been solved?
|
|
1631
|
+
>>> m.optimize()
|
|
1632
|
+
>>> m.var_continuous.solution
|
|
1633
|
+
shape: (3, 2)
|
|
1634
|
+
┌──────┬──────────┐
|
|
1635
|
+
│ dim1 ┆ solution │
|
|
1636
|
+
│ --- ┆ --- │
|
|
1637
|
+
│ i64 ┆ f64 │
|
|
1638
|
+
╞══════╪══════════╡
|
|
1639
|
+
│ 1 ┆ 5.0 │
|
|
1640
|
+
│ 2 ┆ 5.0 │
|
|
1641
|
+
│ 3 ┆ 5.0 │
|
|
1642
|
+
└──────┴──────────┘
|
|
1643
|
+
>>> m.var_integer.solution
|
|
1644
|
+
shape: (3, 2)
|
|
1645
|
+
┌──────┬──────────┐
|
|
1646
|
+
│ dim1 ┆ solution │
|
|
1647
|
+
│ --- ┆ --- │
|
|
1648
|
+
│ i64 ┆ i64 │
|
|
1649
|
+
╞══════╪══════════╡
|
|
1650
|
+
│ 1 ┆ 5 │
|
|
1651
|
+
│ 2 ┆ 5 │
|
|
1652
|
+
│ 3 ┆ 5 │
|
|
1653
|
+
└──────┴──────────┘
|
|
1654
|
+
>>> m.var_dimensionless.solution
|
|
1655
|
+
5
|
|
1656
|
+
"""
|
|
1657
|
+
try:
|
|
1658
|
+
solution = self.attr.Value
|
|
1659
|
+
except RuntimeError as e:
|
|
1660
|
+
raise RuntimeError(
|
|
1661
|
+
"Failed to retrieve solution for variable. Are you sure the model has been solved?"
|
|
1662
|
+
) from e
|
|
1582
1663
|
if isinstance(solution, pl.DataFrame):
|
|
1583
1664
|
solution = solution.rename({"Value": SOLUTION_KEY})
|
|
1665
|
+
|
|
1666
|
+
if self.vtype in [VType.BINARY, VType.INTEGER]:
|
|
1667
|
+
if isinstance(solution, pl.DataFrame):
|
|
1668
|
+
solution = solution.with_columns(
|
|
1669
|
+
pl.col("solution").alias("solution_float"),
|
|
1670
|
+
pl.col("solution").round().cast(pl.Int64),
|
|
1671
|
+
)
|
|
1672
|
+
if Config.integer_tolerance != 0:
|
|
1673
|
+
df = solution.filter(
|
|
1674
|
+
(pl.col("solution_float") - pl.col("solution")).abs()
|
|
1675
|
+
> Config.integer_tolerance
|
|
1676
|
+
)
|
|
1677
|
+
assert df.is_empty(), (
|
|
1678
|
+
f"Variable {self.name} has a non-integer value: {df}\nThis should not happen."
|
|
1679
|
+
)
|
|
1680
|
+
solution = solution.drop("solution_float")
|
|
1681
|
+
else:
|
|
1682
|
+
solution_float = solution
|
|
1683
|
+
solution = int(round(solution))
|
|
1684
|
+
if Config.integer_tolerance != 0:
|
|
1685
|
+
assert abs(solution - solution_float) < Config.integer_tolerance, (
|
|
1686
|
+
f"Value of variable {self.name} is not an integer: {solution}. This should not happen."
|
|
1687
|
+
)
|
|
1688
|
+
|
|
1584
1689
|
return solution
|
|
1585
1690
|
|
|
1586
1691
|
def __repr__(self):
|
|
@@ -1606,10 +1711,7 @@ class Variable(ModelElementWithId, SupportsMath, SupportPolarsMethodMixin):
|
|
|
1606
1711
|
|
|
1607
1712
|
def to_expr(self) -> Expression:
|
|
1608
1713
|
self._assert_has_ids()
|
|
1609
|
-
|
|
1610
|
-
return self._new(self.data.drop(SOLUTION_KEY))
|
|
1611
|
-
else:
|
|
1612
|
-
return self._new(self.data.drop(SOLUTION_KEY, strict=False))
|
|
1714
|
+
return self._new(self.data.drop(SOLUTION_KEY, strict=False))
|
|
1613
1715
|
|
|
1614
1716
|
def _new(self, data: pl.DataFrame):
|
|
1615
1717
|
self._assert_has_ids()
|
|
@@ -1686,12 +1788,7 @@ class Variable(ModelElementWithId, SupportsMath, SupportPolarsMethodMixin):
|
|
|
1686
1788
|
expr = self.to_expr()
|
|
1687
1789
|
data = expr.data.rename({dim: "__prev"})
|
|
1688
1790
|
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
).drop(["__prev", "__next"])
|
|
1693
|
-
else:
|
|
1694
|
-
data = data.join(
|
|
1695
|
-
wrapped, left_on="__prev", right_on="__next", how="inner"
|
|
1696
|
-
).drop(["__prev", "__next"], strict=False)
|
|
1791
|
+
data = data.join(
|
|
1792
|
+
wrapped, left_on="__prev", right_on="__next", how="inner"
|
|
1793
|
+
).drop(["__prev", "__next"], strict=False)
|
|
1697
1794
|
return expr._new(data)
|
pyoframe/model.py
CHANGED
|
@@ -8,6 +8,7 @@ import pyoptinterface as poi
|
|
|
8
8
|
from pyoframe.constants import (
|
|
9
9
|
CONST_TERM,
|
|
10
10
|
SUPPORTED_SOLVER_TYPES,
|
|
11
|
+
SUPPORTED_SOLVERS,
|
|
11
12
|
Config,
|
|
12
13
|
ObjSense,
|
|
13
14
|
ObjSenseValue,
|
|
@@ -34,6 +35,7 @@ class Model:
|
|
|
34
35
|
Gurobi only: a dictionary of parameters to set when creating the Gurobi environment.
|
|
35
36
|
use_var_names:
|
|
36
37
|
Whether to pass variable names to the solver. Set to `True` if you'd like outputs from e.g. `Model.write()` to be legible.
|
|
38
|
+
Does not work with HiGHS (see [here](https://github.com/Bravos-Power/pyoframe/issues/102#issuecomment-2727521430)).
|
|
37
39
|
sense:
|
|
38
40
|
Either "min" or "max". Indicates whether it's a minmization or maximization problem.
|
|
39
41
|
Typically, this parameter can be omitted (`None`) as it will automatically be
|
|
@@ -76,10 +78,10 @@ class Model:
|
|
|
76
78
|
|
|
77
79
|
def __init__(
|
|
78
80
|
self,
|
|
79
|
-
name=None,
|
|
81
|
+
name: Optional[str] = None,
|
|
80
82
|
solver: Optional[SUPPORTED_SOLVER_TYPES] = None,
|
|
81
83
|
solver_env: Optional[Dict[str, str]] = None,
|
|
82
|
-
use_var_names=False,
|
|
84
|
+
use_var_names: bool = False,
|
|
83
85
|
sense: Union[ObjSense, ObjSenseValue, None] = None,
|
|
84
86
|
):
|
|
85
87
|
self.poi, self.solver_name = Model.create_poi_model(solver, solver_env)
|
|
@@ -106,7 +108,7 @@ class Model:
|
|
|
106
108
|
):
|
|
107
109
|
if solver is None:
|
|
108
110
|
if Config.default_solver is None:
|
|
109
|
-
for solver_option in
|
|
111
|
+
for solver_option in SUPPORTED_SOLVERS:
|
|
110
112
|
try:
|
|
111
113
|
return cls.create_poi_model(solver_option, solver_env)
|
|
112
114
|
except RuntimeError:
|
|
@@ -122,13 +124,13 @@ class Model:
|
|
|
122
124
|
from pyoptinterface import gurobi
|
|
123
125
|
|
|
124
126
|
if solver_env is None:
|
|
125
|
-
|
|
127
|
+
env = gurobi.Env()
|
|
126
128
|
else:
|
|
127
129
|
env = gurobi.Env(empty=True)
|
|
128
130
|
for key, value in solver_env.items():
|
|
129
131
|
env.set_raw_parameter(key, value)
|
|
130
132
|
env.start()
|
|
131
|
-
|
|
133
|
+
model = gurobi.Model(env)
|
|
132
134
|
elif solver == "highs":
|
|
133
135
|
from pyoptinterface import highs
|
|
134
136
|
|
|
@@ -233,9 +235,9 @@ class Model:
|
|
|
233
235
|
and __name not in Model._reserved_attributes
|
|
234
236
|
):
|
|
235
237
|
if isinstance(__value, ModelElementWithId):
|
|
236
|
-
assert not hasattr(
|
|
237
|
-
|
|
238
|
-
)
|
|
238
|
+
assert not hasattr(self, __name), (
|
|
239
|
+
f"Cannot create {__name} since it was already created."
|
|
240
|
+
)
|
|
239
241
|
|
|
240
242
|
__value.on_add_to_model(self, __name)
|
|
241
243
|
|
|
@@ -256,12 +258,32 @@ class Model:
|
|
|
256
258
|
objective=bool(self.objective),
|
|
257
259
|
)
|
|
258
260
|
|
|
259
|
-
def write(self, file_path: Union[Path, str]):
|
|
261
|
+
def write(self, file_path: Union[Path, str], pretty: bool = False):
|
|
262
|
+
"""
|
|
263
|
+
Output the model to a file.
|
|
264
|
+
|
|
265
|
+
Typical usage includes writing the solution to a `.sol` file as well as writing the problem to a `.lp` or `.mps` file.
|
|
266
|
+
Set `use_var_names` in your model constructor to `True` if you'd like the output to contain human-readable names (useful for debugging).
|
|
267
|
+
|
|
268
|
+
Parameters:
|
|
269
|
+
file_path:
|
|
270
|
+
The path to the file to write to.
|
|
271
|
+
pretty:
|
|
272
|
+
Only used when writing .sol files in HiGHS. If `True`, will use HiGH's pretty print columnar style which contains more information.
|
|
273
|
+
"""
|
|
260
274
|
file_path = Path(file_path)
|
|
261
275
|
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
262
|
-
|
|
276
|
+
kwargs = {}
|
|
277
|
+
if self.solver_name == "highs":
|
|
278
|
+
if self.use_var_names:
|
|
279
|
+
self.params.write_solution_style = 1
|
|
280
|
+
kwargs["pretty"] = pretty
|
|
281
|
+
self.poi.write(str(file_path), **kwargs)
|
|
263
282
|
|
|
264
283
|
def optimize(self):
|
|
284
|
+
"""
|
|
285
|
+
Optimize the model using your selected solver (e.g. Gurobi, HiGHS).
|
|
286
|
+
"""
|
|
265
287
|
self.poi.optimize()
|
|
266
288
|
|
|
267
289
|
@for_solvers("gurobi")
|
|
@@ -282,11 +304,11 @@ class Model:
|
|
|
282
304
|
>>> m.maximize = 3 * m.X + 2 * m.Y + m.Z
|
|
283
305
|
>>> m.optimize()
|
|
284
306
|
>>> m.X.solution, m.Y.solution, m.Z.solution
|
|
285
|
-
(1
|
|
307
|
+
(1, 9, 0.0)
|
|
286
308
|
>>> m.my_constraint.dual
|
|
287
309
|
Traceback (most recent call last):
|
|
288
310
|
...
|
|
289
|
-
|
|
311
|
+
RuntimeError: Unable to retrieve attribute 'Pi'
|
|
290
312
|
>>> m.convert_to_fixed()
|
|
291
313
|
>>> m.optimize()
|
|
292
314
|
>>> m.my_constraint.dual
|
|
@@ -322,44 +344,44 @@ class Model:
|
|
|
322
344
|
>>> m.bad_constraint.attr.IIS
|
|
323
345
|
Traceback (most recent call last):
|
|
324
346
|
...
|
|
325
|
-
|
|
347
|
+
RuntimeError: Unable to retrieve attribute 'IISConstr'
|
|
326
348
|
>>> m.compute_IIS()
|
|
327
349
|
>>> m.bad_constraint.attr.IIS
|
|
328
350
|
True
|
|
329
351
|
"""
|
|
330
352
|
self.poi.computeIIS()
|
|
331
353
|
|
|
332
|
-
@for_solvers("gurobi")
|
|
333
354
|
def dispose(self):
|
|
334
355
|
"""
|
|
335
|
-
|
|
356
|
+
Disposes of the model and cleans up the solver environment.
|
|
336
357
|
|
|
337
|
-
|
|
358
|
+
When using Gurobi compute server, this cleanup will
|
|
359
|
+
ensure your run is not marked as 'ABORTED'.
|
|
338
360
|
|
|
339
|
-
|
|
340
|
-
Unfortunately, this is a limitation from the underlying solver interface library.
|
|
341
|
-
See https://github.com/metab0t/PyOptInterface/issues/36 for context.
|
|
361
|
+
Note that once the model is disposed, it cannot be used anymore.
|
|
342
362
|
|
|
343
363
|
Examples:
|
|
344
|
-
>>> m = pf.Model(
|
|
364
|
+
>>> m = pf.Model()
|
|
345
365
|
>>> m.X = pf.Variable(ub=1)
|
|
346
366
|
>>> m.maximize = m.X
|
|
347
367
|
>>> m.optimize()
|
|
348
368
|
>>> m.X.solution
|
|
349
369
|
1.0
|
|
350
370
|
>>> m.dispose()
|
|
351
|
-
>>> m.X.solution
|
|
352
|
-
Traceback (most recent call last):
|
|
353
|
-
...
|
|
354
|
-
AttributeError: 'Model' object has no attribute 'poi'
|
|
355
371
|
"""
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
372
|
+
env = None
|
|
373
|
+
if hasattr(self.poi, "_env"):
|
|
374
|
+
env = self.poi._env
|
|
375
|
+
self.poi.close()
|
|
376
|
+
if env is not None:
|
|
377
|
+
env.close()
|
|
378
|
+
|
|
379
|
+
def __del__(self):
|
|
380
|
+
# This ensures that the model is closed *before* the environment is. This avoids the Gurobi warning:
|
|
381
|
+
# Warning: environment still referenced so free is deferred (Continue to use WLS)
|
|
382
|
+
# I include the hasattr check to avoid errors in case __init__ failed and poi was never set.
|
|
383
|
+
if hasattr(self, "poi"):
|
|
384
|
+
self.poi.close()
|
|
363
385
|
|
|
364
386
|
def _set_param(self, name, value):
|
|
365
387
|
self.poi.set_raw_parameter(name, value)
|
|
@@ -370,11 +392,17 @@ class Model:
|
|
|
370
392
|
def _set_attr(self, name, value):
|
|
371
393
|
try:
|
|
372
394
|
self.poi.set_model_attribute(poi.ModelAttribute[name], value)
|
|
373
|
-
except KeyError:
|
|
374
|
-
self.
|
|
395
|
+
except KeyError as e:
|
|
396
|
+
if self.solver_name == "gurobi":
|
|
397
|
+
self.poi.set_model_raw_attribute(name, value)
|
|
398
|
+
else:
|
|
399
|
+
raise e
|
|
375
400
|
|
|
376
401
|
def _get_attr(self, name):
|
|
377
402
|
try:
|
|
378
403
|
return self.poi.get_model_attribute(poi.ModelAttribute[name])
|
|
379
|
-
except KeyError:
|
|
380
|
-
|
|
404
|
+
except KeyError as e:
|
|
405
|
+
if self.solver_name == "gurobi":
|
|
406
|
+
return self.poi.get_model_raw_attribute(name)
|
|
407
|
+
else:
|
|
408
|
+
raise e
|
pyoframe/model_element.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
|
-
from collections import defaultdict
|
|
5
4
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
6
5
|
|
|
7
6
|
import polars as pl
|
|
@@ -22,9 +21,9 @@ if TYPE_CHECKING: # pragma: no cover
|
|
|
22
21
|
class ModelElement(ABC):
|
|
23
22
|
def __init__(self, data: pl.DataFrame, **kwargs) -> None:
|
|
24
23
|
# Sanity checks, no duplicate column names
|
|
25
|
-
assert len(data.columns) == len(
|
|
26
|
-
|
|
27
|
-
)
|
|
24
|
+
assert len(data.columns) == len(set(data.columns)), (
|
|
25
|
+
"Duplicate column names found."
|
|
26
|
+
)
|
|
28
27
|
|
|
29
28
|
cols = _get_dimensions(data)
|
|
30
29
|
if cols is None:
|
pyoframe/util.py
CHANGED
|
@@ -379,9 +379,9 @@ class NamedVariableMapper:
|
|
|
379
379
|
|
|
380
380
|
def _element_to_map(self, element) -> pl.DataFrame:
|
|
381
381
|
element_name = element.name # type: ignore
|
|
382
|
-
assert (
|
|
383
|
-
|
|
384
|
-
)
|
|
382
|
+
assert element_name is not None, (
|
|
383
|
+
"Element must have a name to be used in a named mapping."
|
|
384
|
+
)
|
|
385
385
|
element._assert_has_ids()
|
|
386
386
|
return concat_dimensions(
|
|
387
387
|
element.data.select(element.dimensions_unsafe + [VAR_KEY]),
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: pyoframe
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Blazing fast linear program interface
|
|
5
5
|
Author-email: Bravos Power <dev@bravospower.com>
|
|
6
|
+
License-Expression: MIT
|
|
6
7
|
Project-URL: Homepage, https://bravos-power.github.io/pyoframe/
|
|
7
8
|
Project-URL: documentation, https://bravos-power.github.io/pyoframe/
|
|
8
9
|
Project-URL: repository, https://github.com/Bravos-Power/pyoframe/
|
|
@@ -10,40 +11,40 @@ Project-URL: Issues, https://github.com/Bravos-Power/pyoframe/issues
|
|
|
10
11
|
Classifier: Programming Language :: Python :: 3
|
|
11
12
|
Classifier: Operating System :: OS Independent
|
|
12
13
|
Classifier: Development Status :: 3 - Alpha
|
|
13
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
14
14
|
Classifier: Natural Language :: English
|
|
15
15
|
Requires-Python: >=3.9
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
License-File: LICENSE
|
|
18
|
-
Requires-Dist: polars
|
|
18
|
+
Requires-Dist: polars~=1.0
|
|
19
19
|
Requires-Dist: numpy
|
|
20
20
|
Requires-Dist: pyarrow
|
|
21
21
|
Requires-Dist: pandas
|
|
22
|
-
Requires-Dist:
|
|
23
|
-
Requires-Dist: pyoptinterface~=0.4
|
|
22
|
+
Requires-Dist: pyoptinterface<1,>=0.4.1
|
|
24
23
|
Provides-Extra: dev
|
|
25
|
-
Requires-Dist:
|
|
24
|
+
Requires-Dist: ruff; extra == "dev"
|
|
25
|
+
Requires-Dist: polars>=1.30.0; extra == "dev"
|
|
26
26
|
Requires-Dist: bumpver; extra == "dev"
|
|
27
|
-
Requires-Dist: isort; extra == "dev"
|
|
28
27
|
Requires-Dist: pip-tools; extra == "dev"
|
|
29
28
|
Requires-Dist: pytest; extra == "dev"
|
|
30
29
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
31
30
|
Requires-Dist: pre-commit; extra == "dev"
|
|
32
31
|
Requires-Dist: gurobipy; extra == "dev"
|
|
33
32
|
Requires-Dist: highsbox; extra == "dev"
|
|
34
|
-
Requires-Dist: pre-commit; extra == "dev"
|
|
35
33
|
Requires-Dist: coverage; extra == "dev"
|
|
36
|
-
|
|
37
|
-
Requires-Dist:
|
|
38
|
-
Requires-Dist:
|
|
39
|
-
Requires-Dist:
|
|
40
|
-
Requires-Dist: mkdocs-git-
|
|
41
|
-
Requires-Dist: mkdocs-
|
|
42
|
-
Requires-Dist: mkdocs-
|
|
43
|
-
Requires-Dist: mkdocs-
|
|
44
|
-
Requires-Dist: mkdocs-
|
|
34
|
+
Requires-Dist: ipykernel; extra == "dev"
|
|
35
|
+
Requires-Dist: pytest-markdown-docs; extra == "dev"
|
|
36
|
+
Requires-Dist: mkdocs-material==9.*; extra == "dev"
|
|
37
|
+
Requires-Dist: mkdocstrings[python]; extra == "dev"
|
|
38
|
+
Requires-Dist: mkdocs-git-revision-date-localized-plugin; extra == "dev"
|
|
39
|
+
Requires-Dist: mkdocs-git-committers-plugin-2; extra == "dev"
|
|
40
|
+
Requires-Dist: mkdocs-gen-files; extra == "dev"
|
|
41
|
+
Requires-Dist: mkdocs-section-index; extra == "dev"
|
|
42
|
+
Requires-Dist: mkdocs-literate-nav; extra == "dev"
|
|
43
|
+
Requires-Dist: mkdocs-table-reader-plugin; extra == "dev"
|
|
44
|
+
Requires-Dist: markdown-hide-code>=0.1.1; extra == "dev"
|
|
45
45
|
Provides-Extra: highs
|
|
46
46
|
Requires-Dist: highsbox; extra == "highs"
|
|
47
|
+
Dynamic: license-file
|
|
47
48
|
|
|
48
49
|
# Pyoframe: Fast and low-memory linear programming models
|
|
49
50
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
pyoframe/__init__.py,sha256=YswFUwm6GX98dXeT99hqxWqYWLELS71JZf1OpT1kvCg,619
|
|
2
|
+
pyoframe/_arithmetic.py,sha256=agJm2Sl4EjEG7q4n2YHka4mGfCQI3LjOXLaW6oCfGiQ,17222
|
|
3
|
+
pyoframe/_version.py,sha256=iB5DfB5V6YB5Wo4JmvS-txT42QtmGaWcWp3udRT7zCI,511
|
|
4
|
+
pyoframe/constants.py,sha256=WBCmhunavNVwJcmg9ojnA6TVJCLSrgWVE4YKZnhZNz4,4192
|
|
5
|
+
pyoframe/core.py,sha256=kK-eMI7teakAAG4JjOiwRBsCgNJIlOxR0b4JOUlQHs4,67239
|
|
6
|
+
pyoframe/model.py,sha256=a7pEwagVxHC1ZUMr8ifO4n0ca5Ways3wip-Wps0rlcg,14257
|
|
7
|
+
pyoframe/model_element.py,sha256=YmAdx4yM5irGTiZ5uQmDa-u05QdFKngIFy8qNnogvzo,5911
|
|
8
|
+
pyoframe/monkey_patch.py,sha256=9IfS14G6IPabmM9z80jzi_D4Rq0Mdx5aUCA39Yi2tgE,2044
|
|
9
|
+
pyoframe/objective.py,sha256=PBWxj30QkFlsvY6ijZ6KjyKdrJARD4to0ieF6GUqaQU,3238
|
|
10
|
+
pyoframe/util.py,sha256=dHIwAyyD9wn36yM8IOlrboTGUGA7STq3IBTxfYSOPjU,13480
|
|
11
|
+
pyoframe-0.2.0.dist-info/licenses/LICENSE,sha256=u_Spw4ynlwTMRZeCX-uacv_hBU547pBygiA6d2ONNV4,1074
|
|
12
|
+
pyoframe-0.2.0.dist-info/METADATA,sha256=GTwsHP14u5Zwb1E4pnjyeCS9-0DVmLXjNlo3ebZiSbw,3607
|
|
13
|
+
pyoframe-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
14
|
+
pyoframe-0.2.0.dist-info/top_level.txt,sha256=10z3OOJSVLriQ0IrFLMH8CH9zByugPWolqhlHlkNjV4,9
|
|
15
|
+
pyoframe-0.2.0.dist-info/RECORD,,
|
pyoframe-0.1.4.dist-info/RECORD
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
pyoframe/__init__.py,sha256=nEN0OgqhevtsvxEiPbJLzwPojf3ngYAoT90M_1mc4kM,477
|
|
2
|
-
pyoframe/_arithmetic.py,sha256=vqzMZip1kTkUUMyHohUyKJxx2nEeQIH7N57fwevLWaQ,17284
|
|
3
|
-
pyoframe/constants.py,sha256=Dy1sCzykZlmkbvgsc5ckujqXPuYmsKkD3stANU0qr5Y,3784
|
|
4
|
-
pyoframe/core.py,sha256=4heBn7RGOWV-Yg0OoiLr8sGBaygpTx-D-pLuC4nC0Dw,62865
|
|
5
|
-
pyoframe/model.py,sha256=7nVH2oZeTuk53l76PLNElsiHEgmbTvrf7lGIa9_CYm4,13011
|
|
6
|
-
pyoframe/model_element.py,sha256=RBmsE2FOct3UL2N2LZLfWYv2wL_QKqNmKmmR_pD_ERs,5945
|
|
7
|
-
pyoframe/monkey_patch.py,sha256=9IfS14G6IPabmM9z80jzi_D4Rq0Mdx5aUCA39Yi2tgE,2044
|
|
8
|
-
pyoframe/objective.py,sha256=PBWxj30QkFlsvY6ijZ6KjyKdrJARD4to0ieF6GUqaQU,3238
|
|
9
|
-
pyoframe/util.py,sha256=Oyk8xh6FJHlb04X_cM4lN0UzdnKLXAMrKfyOf7IexiA,13480
|
|
10
|
-
pyoframe-0.1.4.dist-info/LICENSE,sha256=dkwA40ZzT-3x6eu2a6mf_o7PNSqHbdsyaFNhLxGHNQs,1065
|
|
11
|
-
pyoframe-0.1.4.dist-info/METADATA,sha256=v3ZH_JvXSg4cN515Qo0FvZviXIC0l-Ja5tgq5IdgwLI,3558
|
|
12
|
-
pyoframe-0.1.4.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
13
|
-
pyoframe-0.1.4.dist-info/top_level.txt,sha256=10z3OOJSVLriQ0IrFLMH8CH9zByugPWolqhlHlkNjV4,9
|
|
14
|
-
pyoframe-0.1.4.dist-info/RECORD,,
|
|
File without changes
|