pyoframe 0.1.4__py3-none-any.whl → 0.2.1__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 +34 -0
- pyoframe/constants.py +12 -7
- pyoframe/core.py +191 -101
- 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.1.dist-info}/METADATA +19 -18
- pyoframe-0.2.1.dist-info/RECORD +15 -0
- {pyoframe-0.1.4.dist-info → pyoframe-0.2.1.dist-info}/WHEEL +1 -1
- {pyoframe-0.1.4.dist-info → pyoframe-0.2.1.dist-info/licenses}/LICENSE +1 -1
- pyoframe-0.1.4.dist-info/RECORD +0 -14
- {pyoframe-0.1.4.dist-info → pyoframe-0.2.1.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,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '0.2.1'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 2, 1)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
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
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import warnings
|
|
4
3
|
from abc import ABC, abstractmethod
|
|
5
4
|
from typing import (
|
|
6
5
|
TYPE_CHECKING,
|
|
@@ -33,7 +32,6 @@ from pyoframe.constants import (
|
|
|
33
32
|
CONSTRAINT_KEY,
|
|
34
33
|
DUAL_KEY,
|
|
35
34
|
KEY_TYPE,
|
|
36
|
-
POLARS_VERSION,
|
|
37
35
|
QUAD_VAR_KEY,
|
|
38
36
|
RESERVED_COL_KEYS,
|
|
39
37
|
SOLUTION_KEY,
|
|
@@ -294,9 +292,9 @@ class Set(ModelElement, SupportsMath, SupportPolarsMethodMixin):
|
|
|
294
292
|
over_merged = over_frames[0]
|
|
295
293
|
|
|
296
294
|
for df in over_frames[1:]:
|
|
297
|
-
assert (
|
|
298
|
-
|
|
299
|
-
)
|
|
295
|
+
assert set(over_merged.columns) & set(df.columns) == set(), (
|
|
296
|
+
"All coordinates must have unique column names."
|
|
297
|
+
)
|
|
300
298
|
over_merged = over_merged.join(df, how="cross")
|
|
301
299
|
return over_merged
|
|
302
300
|
|
|
@@ -309,9 +307,9 @@ class Set(ModelElement, SupportsMath, SupportPolarsMethodMixin):
|
|
|
309
307
|
|
|
310
308
|
def __mul__(self, other):
|
|
311
309
|
if isinstance(other, Set):
|
|
312
|
-
assert (
|
|
313
|
-
|
|
314
|
-
)
|
|
310
|
+
assert set(self.data.columns) & set(other.data.columns) == set(), (
|
|
311
|
+
"Cannot multiply two sets with columns in common."
|
|
312
|
+
)
|
|
315
313
|
return Set(self.data, other.data)
|
|
316
314
|
return super().__mul__(other)
|
|
317
315
|
|
|
@@ -346,18 +344,11 @@ class Set(ModelElement, SupportsMath, SupportPolarsMethodMixin):
|
|
|
346
344
|
elif isinstance(set, Constraint):
|
|
347
345
|
df = set.data.select(set.dimensions_unsafe)
|
|
348
346
|
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
|
-
)
|
|
347
|
+
df = (
|
|
348
|
+
set.to_expr()
|
|
349
|
+
.data.drop(RESERVED_COL_KEYS, strict=False)
|
|
350
|
+
.unique(maintain_order=True)
|
|
351
|
+
)
|
|
361
352
|
elif isinstance(set, pd.Index):
|
|
362
353
|
df = pl.from_pandas(pd.DataFrame(index=set).reset_index())
|
|
363
354
|
elif isinstance(set, pd.DataFrame):
|
|
@@ -620,13 +611,13 @@ class Expression(ModelElement, SupportsMath, SupportPolarsMethodMixin):
|
|
|
620
611
|
"""
|
|
621
612
|
df: pl.DataFrame = Set(set).data
|
|
622
613
|
set_dims = _get_dimensions(df)
|
|
623
|
-
assert (
|
|
624
|
-
|
|
625
|
-
)
|
|
614
|
+
assert set_dims is not None, (
|
|
615
|
+
"Cannot use .within() with a set with no dimensions."
|
|
616
|
+
)
|
|
626
617
|
dims = self.dimensions
|
|
627
|
-
assert (
|
|
628
|
-
|
|
629
|
-
)
|
|
618
|
+
assert dims is not None, (
|
|
619
|
+
"Cannot use .within() with an expression with no dimensions."
|
|
620
|
+
)
|
|
630
621
|
dims_in_common = [dim for dim in dims if dim in set_dims]
|
|
631
622
|
by_dims = df.select(dims_in_common).unique(maintain_order=True)
|
|
632
623
|
return self._new(self.data.join(by_dims, on=dims_in_common))
|
|
@@ -800,15 +791,9 @@ class Expression(ModelElement, SupportsMath, SupportPolarsMethodMixin):
|
|
|
800
791
|
keys = keys.with_columns(
|
|
801
792
|
pl.lit(CONST_TERM).alias(QUAD_VAR_KEY).cast(KEY_TYPE)
|
|
802
793
|
)
|
|
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))
|
|
794
|
+
data = data.join(
|
|
795
|
+
keys, on=dim + self._variable_columns, how="full", coalesce=True
|
|
796
|
+
).with_columns(pl.col(COEF_KEY).fill_null(0.0))
|
|
812
797
|
|
|
813
798
|
data = data.with_columns(
|
|
814
799
|
pl.when(pl.col(VAR_KEY) == CONST_TERM)
|
|
@@ -826,10 +811,7 @@ class Expression(ModelElement, SupportsMath, SupportPolarsMethodMixin):
|
|
|
826
811
|
constant_terms = constant_terms.drop(QUAD_VAR_KEY)
|
|
827
812
|
if dims is not None:
|
|
828
813
|
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")
|
|
814
|
+
df = constant_terms.join(dims_df, on=dims, how="full", coalesce=True)
|
|
833
815
|
return df.with_columns(pl.col(COEF_KEY).fill_null(0.0))
|
|
834
816
|
else:
|
|
835
817
|
if len(constant_terms) == 0:
|
|
@@ -870,9 +852,9 @@ class Expression(ModelElement, SupportsMath, SupportPolarsMethodMixin):
|
|
|
870
852
|
>>> m.expr_2.evaluate()
|
|
871
853
|
63.0
|
|
872
854
|
"""
|
|
873
|
-
assert (
|
|
874
|
-
|
|
875
|
-
)
|
|
855
|
+
assert self._model is not None, (
|
|
856
|
+
"Expression must be added to the model to use .value"
|
|
857
|
+
)
|
|
876
858
|
|
|
877
859
|
df = self.data
|
|
878
860
|
sm = self._model.poi
|
|
@@ -899,16 +881,23 @@ class Expression(ModelElement, SupportsMath, SupportPolarsMethodMixin):
|
|
|
899
881
|
df = df.group_by(dims, maintain_order=True)
|
|
900
882
|
return df.sum()
|
|
901
883
|
|
|
902
|
-
def to_poi(self) -> poi.ScalarAffineFunction:
|
|
884
|
+
def to_poi(self) -> poi.ScalarAffineFunction | poi.ScalarQuadraticFunction:
|
|
903
885
|
if self.dimensions is not None:
|
|
904
886
|
raise ValueError(
|
|
905
887
|
"Only non-dimensioned expressions can be converted to PyOptInterface."
|
|
906
888
|
) # pragma: no cover
|
|
907
889
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
890
|
+
if self.is_quadratic:
|
|
891
|
+
return poi.ScalarQuadraticFunction(
|
|
892
|
+
coefficients=self.data.get_column(COEF_KEY).to_numpy(),
|
|
893
|
+
var1s=self.data.get_column(VAR_KEY).to_numpy(),
|
|
894
|
+
var2s=self.data.get_column(QUAD_VAR_KEY).to_numpy(),
|
|
895
|
+
)
|
|
896
|
+
else:
|
|
897
|
+
return poi.ScalarAffineFunction(
|
|
898
|
+
coefficients=self.data.get_column(COEF_KEY).to_numpy(),
|
|
899
|
+
variables=self.data.get_column(VAR_KEY).to_numpy(),
|
|
900
|
+
)
|
|
912
901
|
|
|
913
902
|
def to_str_table(self, include_const_term=True):
|
|
914
903
|
data = self.data if include_const_term else self.variable_terms
|
|
@@ -951,10 +940,10 @@ class Expression(ModelElement, SupportsMath, SupportPolarsMethodMixin):
|
|
|
951
940
|
|
|
952
941
|
if dimensions is not None:
|
|
953
942
|
data = data.group_by(dimensions, maintain_order=True).agg(
|
|
954
|
-
pl.col("expr").str.
|
|
943
|
+
pl.col("expr").str.join(delimiter=" ")
|
|
955
944
|
)
|
|
956
945
|
else:
|
|
957
|
-
data = data.select(pl.col("expr").str.
|
|
946
|
+
data = data.select(pl.col("expr").str.join(delimiter=" "))
|
|
958
947
|
|
|
959
948
|
# Remove leading +
|
|
960
949
|
data = data.with_columns(pl.col("expr").str.strip_chars(characters=" +"))
|
|
@@ -1010,7 +999,7 @@ class Expression(ModelElement, SupportsMath, SupportPolarsMethodMixin):
|
|
|
1010
999
|
include_const_term=include_const_term,
|
|
1011
1000
|
)
|
|
1012
1001
|
str_table = self.to_str_create_prefix(str_table)
|
|
1013
|
-
result += str_table.select(pl.col("expr").str.
|
|
1002
|
+
result += str_table.select(pl.col("expr").str.join(delimiter="\n")).item()
|
|
1014
1003
|
|
|
1015
1004
|
return result
|
|
1016
1005
|
|
|
@@ -1054,6 +1043,31 @@ def sum(
|
|
|
1054
1043
|
over: Union[str, Sequence[str], SupportsToExpr],
|
|
1055
1044
|
expr: Optional[SupportsToExpr] = None,
|
|
1056
1045
|
) -> "Expression":
|
|
1046
|
+
"""
|
|
1047
|
+
Sum an expression over specified dimensions.
|
|
1048
|
+
If no dimensions are specified, the sum is taken over all of the expression's dimensions.
|
|
1049
|
+
|
|
1050
|
+
Examples:
|
|
1051
|
+
>>> expr = pl.DataFrame({
|
|
1052
|
+
... "time": ["mon", "tue", "wed", "mon", "tue"],
|
|
1053
|
+
... "place": ["Toronto", "Toronto", "Toronto", "Vancouver", "Vancouver"],
|
|
1054
|
+
... "tiktok_posts": [1e6, 3e6, 2e6, 1e6, 2e6]
|
|
1055
|
+
... }).to_expr()
|
|
1056
|
+
>>> expr
|
|
1057
|
+
<Expression size=5 dimensions={'time': 3, 'place': 2} terms=5>
|
|
1058
|
+
[mon,Toronto]: 1000000
|
|
1059
|
+
[tue,Toronto]: 3000000
|
|
1060
|
+
[wed,Toronto]: 2000000
|
|
1061
|
+
[mon,Vancouver]: 1000000
|
|
1062
|
+
[tue,Vancouver]: 2000000
|
|
1063
|
+
>>> pf.sum("time", expr)
|
|
1064
|
+
<Expression size=2 dimensions={'place': 2} terms=2>
|
|
1065
|
+
[Toronto]: 6000000
|
|
1066
|
+
[Vancouver]: 3000000
|
|
1067
|
+
>>> pf.sum(expr)
|
|
1068
|
+
<Expression size=1 dimensions={} terms=1>
|
|
1069
|
+
9000000
|
|
1070
|
+
"""
|
|
1057
1071
|
if expr is None:
|
|
1058
1072
|
assert isinstance(over, SupportsMath)
|
|
1059
1073
|
over = over.to_expr()
|
|
@@ -1069,13 +1083,34 @@ def sum(
|
|
|
1069
1083
|
|
|
1070
1084
|
|
|
1071
1085
|
def sum_by(by: Union[str, Sequence[str]], expr: SupportsToExpr) -> "Expression":
|
|
1086
|
+
"""
|
|
1087
|
+
Like `pf.sum()`, but the sum is taken over all dimensions except those specified in `by` (just like a groupby operation).
|
|
1088
|
+
|
|
1089
|
+
Examples:
|
|
1090
|
+
>>> expr = pl.DataFrame({
|
|
1091
|
+
... "time": ["mon", "tue", "wed", "mon", "tue"],
|
|
1092
|
+
... "place": ["Toronto", "Toronto", "Toronto", "Vancouver", "Vancouver"],
|
|
1093
|
+
... "tiktok_posts": [1e6, 3e6, 2e6, 1e6, 2e6]
|
|
1094
|
+
... }).to_expr()
|
|
1095
|
+
>>> expr
|
|
1096
|
+
<Expression size=5 dimensions={'time': 3, 'place': 2} terms=5>
|
|
1097
|
+
[mon,Toronto]: 1000000
|
|
1098
|
+
[tue,Toronto]: 3000000
|
|
1099
|
+
[wed,Toronto]: 2000000
|
|
1100
|
+
[mon,Vancouver]: 1000000
|
|
1101
|
+
[tue,Vancouver]: 2000000
|
|
1102
|
+
>>> pf.sum_by("place", expr)
|
|
1103
|
+
<Expression size=2 dimensions={'place': 2} terms=2>
|
|
1104
|
+
[Toronto]: 6000000
|
|
1105
|
+
[Vancouver]: 3000000
|
|
1106
|
+
"""
|
|
1072
1107
|
if isinstance(by, str):
|
|
1073
1108
|
by = [by]
|
|
1074
1109
|
expr = expr.to_expr()
|
|
1075
1110
|
dimensions = expr.dimensions
|
|
1076
|
-
assert (
|
|
1077
|
-
dimensions
|
|
1078
|
-
)
|
|
1111
|
+
assert dimensions is not None, (
|
|
1112
|
+
"Cannot sum by dimensions with an expression with no dimensions."
|
|
1113
|
+
)
|
|
1079
1114
|
remaining_dims = [dim for dim in dimensions if dim not in by]
|
|
1080
1115
|
return sum(over=remaining_dims, expr=expr)
|
|
1081
1116
|
|
|
@@ -1112,16 +1147,22 @@ class Constraint(ModelElementWithId):
|
|
|
1112
1147
|
except KeyError:
|
|
1113
1148
|
setter = self._model.poi.set_constraint_raw_attribute
|
|
1114
1149
|
|
|
1150
|
+
constr_type = (
|
|
1151
|
+
poi.ConstraintType.Quadratic
|
|
1152
|
+
if self.lhs.is_quadratic
|
|
1153
|
+
else poi.ConstraintType.Linear
|
|
1154
|
+
)
|
|
1155
|
+
|
|
1115
1156
|
if self.dimensions is None:
|
|
1116
1157
|
for key in self.data.get_column(CONSTRAINT_KEY):
|
|
1117
|
-
setter(poi.ConstraintIndex(
|
|
1158
|
+
setter(poi.ConstraintIndex(constr_type, key), name, value)
|
|
1118
1159
|
else:
|
|
1119
1160
|
for key, value in (
|
|
1120
1161
|
self.data.join(value, on=self.dimensions, how="inner")
|
|
1121
1162
|
.select(pl.col(CONSTRAINT_KEY), pl.col(col_name))
|
|
1122
1163
|
.iter_rows()
|
|
1123
1164
|
):
|
|
1124
|
-
setter(poi.ConstraintIndex(
|
|
1165
|
+
setter(poi.ConstraintIndex(constr_type, key), name, value)
|
|
1125
1166
|
|
|
1126
1167
|
@unwrap_single_values
|
|
1127
1168
|
def _get_attribute(self, name):
|
|
@@ -1133,21 +1174,16 @@ class Constraint(ModelElementWithId):
|
|
|
1133
1174
|
except KeyError:
|
|
1134
1175
|
getter = self._model.poi.get_constraint_raw_attribute
|
|
1135
1176
|
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
poi.ConstraintIndex(poi.ConstraintType.Linear, v_id), name
|
|
1147
|
-
)
|
|
1148
|
-
)
|
|
1149
|
-
.alias(col_name)
|
|
1150
|
-
).select(self.dimensions_unsafe + [col_name])
|
|
1177
|
+
constr_type = (
|
|
1178
|
+
poi.ConstraintType.Quadratic
|
|
1179
|
+
if self.lhs.is_quadratic
|
|
1180
|
+
else poi.ConstraintType.Linear
|
|
1181
|
+
)
|
|
1182
|
+
|
|
1183
|
+
ids = self.data.get_column(CONSTRAINT_KEY).to_list()
|
|
1184
|
+
attr = [getter(poi.ConstraintIndex(constr_type, v_id), name) for v_id in ids]
|
|
1185
|
+
data = self.data.with_columns(pl.Series(attr).alias(col_name))
|
|
1186
|
+
return data.select(self.dimensions_unsafe + [col_name])
|
|
1151
1187
|
|
|
1152
1188
|
def on_add_to_model(self, model: "Model", name: str):
|
|
1153
1189
|
super().on_add_to_model(model, name)
|
|
@@ -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()
|
|
@@ -1510,17 +1546,10 @@ class Variable(ModelElementWithId, SupportsMath, SupportPolarsMethodMixin):
|
|
|
1510
1546
|
except KeyError:
|
|
1511
1547
|
getter = self._model.poi.get_variable_raw_attribute
|
|
1512
1548
|
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
action="ignore", category=pl.exceptions.MapWithoutReturnDtypeWarning
|
|
1518
|
-
)
|
|
1519
|
-
return self.data.with_columns(
|
|
1520
|
-
pl.col(VAR_KEY)
|
|
1521
|
-
.map_elements(lambda v_id: getter(poi.VariableIndex(v_id), name))
|
|
1522
|
-
.alias(col_name)
|
|
1523
|
-
).select(self.dimensions_unsafe + [col_name])
|
|
1549
|
+
ids = self.data.get_column(VAR_KEY).to_list()
|
|
1550
|
+
attr = [getter(poi.VariableIndex(v_id), name) for v_id in ids]
|
|
1551
|
+
data = self.data.with_columns(pl.Series(attr).alias(col_name))
|
|
1552
|
+
return data.select(self.dimensions_unsafe + [col_name])
|
|
1524
1553
|
|
|
1525
1554
|
def _assign_ids(self):
|
|
1526
1555
|
kwargs = dict(domain=self.vtype.to_poi())
|
|
@@ -1578,9 +1607,78 @@ class Variable(ModelElementWithId, SupportsMath, SupportPolarsMethodMixin):
|
|
|
1578
1607
|
@property
|
|
1579
1608
|
@unwrap_single_values
|
|
1580
1609
|
def solution(self):
|
|
1581
|
-
|
|
1610
|
+
"""
|
|
1611
|
+
Retrieve a variable's optimal value after the model has been solved.
|
|
1612
|
+
Returned as a DataFrame if the variable has dimensions, otherwise as a single value.
|
|
1613
|
+
Binary and integer variables are returned as integers.
|
|
1614
|
+
|
|
1615
|
+
Examples:
|
|
1616
|
+
>>> m = pf.Model()
|
|
1617
|
+
>>> m.var_continuous = pf.Variable({"dim1": [1, 2, 3]}, lb=5, ub=5)
|
|
1618
|
+
>>> m.var_integer = pf.Variable({"dim1": [1, 2, 3]}, lb=4.5, ub=5.5, vtype=VType.INTEGER)
|
|
1619
|
+
>>> m.var_dimensionless = pf.Variable(lb=4.5, ub=5.5, vtype=VType.INTEGER)
|
|
1620
|
+
>>> m.var_continuous.solution
|
|
1621
|
+
Traceback (most recent call last):
|
|
1622
|
+
...
|
|
1623
|
+
RuntimeError: Failed to retrieve solution for variable. Are you sure the model has been solved?
|
|
1624
|
+
>>> m.optimize()
|
|
1625
|
+
>>> m.var_continuous.solution
|
|
1626
|
+
shape: (3, 2)
|
|
1627
|
+
┌──────┬──────────┐
|
|
1628
|
+
│ dim1 ┆ solution │
|
|
1629
|
+
│ --- ┆ --- │
|
|
1630
|
+
│ i64 ┆ f64 │
|
|
1631
|
+
╞══════╪══════════╡
|
|
1632
|
+
│ 1 ┆ 5.0 │
|
|
1633
|
+
│ 2 ┆ 5.0 │
|
|
1634
|
+
│ 3 ┆ 5.0 │
|
|
1635
|
+
└──────┴──────────┘
|
|
1636
|
+
>>> m.var_integer.solution
|
|
1637
|
+
shape: (3, 2)
|
|
1638
|
+
┌──────┬──────────┐
|
|
1639
|
+
│ dim1 ┆ solution │
|
|
1640
|
+
│ --- ┆ --- │
|
|
1641
|
+
│ i64 ┆ i64 │
|
|
1642
|
+
╞══════╪══════════╡
|
|
1643
|
+
│ 1 ┆ 5 │
|
|
1644
|
+
│ 2 ┆ 5 │
|
|
1645
|
+
│ 3 ┆ 5 │
|
|
1646
|
+
└──────┴──────────┘
|
|
1647
|
+
>>> m.var_dimensionless.solution
|
|
1648
|
+
5
|
|
1649
|
+
"""
|
|
1650
|
+
try:
|
|
1651
|
+
solution = self.attr.Value
|
|
1652
|
+
except RuntimeError as e:
|
|
1653
|
+
raise RuntimeError(
|
|
1654
|
+
"Failed to retrieve solution for variable. Are you sure the model has been solved?"
|
|
1655
|
+
) from e
|
|
1582
1656
|
if isinstance(solution, pl.DataFrame):
|
|
1583
1657
|
solution = solution.rename({"Value": SOLUTION_KEY})
|
|
1658
|
+
|
|
1659
|
+
if self.vtype in [VType.BINARY, VType.INTEGER]:
|
|
1660
|
+
if isinstance(solution, pl.DataFrame):
|
|
1661
|
+
solution = solution.with_columns(
|
|
1662
|
+
pl.col("solution").alias("solution_float"),
|
|
1663
|
+
pl.col("solution").round().cast(pl.Int64),
|
|
1664
|
+
)
|
|
1665
|
+
if Config.integer_tolerance != 0:
|
|
1666
|
+
df = solution.filter(
|
|
1667
|
+
(pl.col("solution_float") - pl.col("solution")).abs()
|
|
1668
|
+
> Config.integer_tolerance
|
|
1669
|
+
)
|
|
1670
|
+
assert df.is_empty(), (
|
|
1671
|
+
f"Variable {self.name} has a non-integer value: {df}\nThis should not happen."
|
|
1672
|
+
)
|
|
1673
|
+
solution = solution.drop("solution_float")
|
|
1674
|
+
else:
|
|
1675
|
+
solution_float = solution
|
|
1676
|
+
solution = int(round(solution))
|
|
1677
|
+
if Config.integer_tolerance != 0:
|
|
1678
|
+
assert abs(solution - solution_float) < Config.integer_tolerance, (
|
|
1679
|
+
f"Value of variable {self.name} is not an integer: {solution}. This should not happen."
|
|
1680
|
+
)
|
|
1681
|
+
|
|
1584
1682
|
return solution
|
|
1585
1683
|
|
|
1586
1684
|
def __repr__(self):
|
|
@@ -1606,10 +1704,7 @@ class Variable(ModelElementWithId, SupportsMath, SupportPolarsMethodMixin):
|
|
|
1606
1704
|
|
|
1607
1705
|
def to_expr(self) -> Expression:
|
|
1608
1706
|
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))
|
|
1707
|
+
return self._new(self.data.drop(SOLUTION_KEY, strict=False))
|
|
1613
1708
|
|
|
1614
1709
|
def _new(self, data: pl.DataFrame):
|
|
1615
1710
|
self._assert_has_ids()
|
|
@@ -1686,12 +1781,7 @@ class Variable(ModelElementWithId, SupportsMath, SupportPolarsMethodMixin):
|
|
|
1686
1781
|
expr = self.to_expr()
|
|
1687
1782
|
data = expr.data.rename({dim: "__prev"})
|
|
1688
1783
|
|
|
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)
|
|
1784
|
+
data = data.join(
|
|
1785
|
+
wrapped, left_on="__prev", right_on="__next", how="inner"
|
|
1786
|
+
).drop(["__prev", "__next"], strict=False)
|
|
1697
1787
|
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.1
|
|
3
|
+
Version: 0.2.1
|
|
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=vYqoJTG51NOUmYyL0xt8asRK8vUT4lGAdal_EZ59mvw,704
|
|
4
|
+
pyoframe/constants.py,sha256=WBCmhunavNVwJcmg9ojnA6TVJCLSrgWVE4YKZnhZNz4,4192
|
|
5
|
+
pyoframe/core.py,sha256=fjCu4eY7QJSFvVfCNtMq-o_spoo76FWO4AviCssHGoo,66925
|
|
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.1.dist-info/licenses/LICENSE,sha256=u_Spw4ynlwTMRZeCX-uacv_hBU547pBygiA6d2ONNV4,1074
|
|
12
|
+
pyoframe-0.2.1.dist-info/METADATA,sha256=_rmMdRjfEkv-1xn9UkjVubVxNUqSKgluVmXz7nSaRnA,3607
|
|
13
|
+
pyoframe-0.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
14
|
+
pyoframe-0.2.1.dist-info/top_level.txt,sha256=10z3OOJSVLriQ0IrFLMH8CH9zByugPWolqhlHlkNjV4,9
|
|
15
|
+
pyoframe-0.2.1.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
|