pyoframe 0.0.11__tar.gz → 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyoframe-0.0.11 → pyoframe-0.1.0}/LICENSE +0 -2
- {pyoframe-0.0.11/src/pyoframe.egg-info → pyoframe-0.1.0}/PKG-INFO +13 -11
- {pyoframe-0.0.11 → pyoframe-0.1.0}/README.md +4 -8
- {pyoframe-0.0.11 → pyoframe-0.1.0}/pyproject.toml +15 -4
- {pyoframe-0.0.11 → pyoframe-0.1.0}/src/pyoframe/__init__.py +3 -4
- {pyoframe-0.0.11 → pyoframe-0.1.0}/src/pyoframe/_arithmetic.py +170 -4
- pyoframe-0.1.0/src/pyoframe/constants.py +135 -0
- {pyoframe-0.0.11 → pyoframe-0.1.0}/src/pyoframe/core.py +746 -421
- pyoframe-0.1.0/src/pyoframe/model.py +283 -0
- {pyoframe-0.0.11 → pyoframe-0.1.0}/src/pyoframe/model_element.py +43 -84
- {pyoframe-0.0.11 → pyoframe-0.1.0}/src/pyoframe/monkey_patch.py +3 -3
- pyoframe-0.1.0/src/pyoframe/objective.py +95 -0
- {pyoframe-0.0.11 → pyoframe-0.1.0}/src/pyoframe/util.py +138 -19
- {pyoframe-0.0.11 → pyoframe-0.1.0/src/pyoframe.egg-info}/PKG-INFO +13 -11
- {pyoframe-0.0.11 → pyoframe-0.1.0}/src/pyoframe.egg-info/SOURCES.txt +1 -4
- {pyoframe-0.0.11 → pyoframe-0.1.0}/src/pyoframe.egg-info/requires.txt +7 -1
- {pyoframe-0.0.11 → pyoframe-0.1.0}/tests/test_arithmetic.py +78 -57
- pyoframe-0.1.0/tests/test_examples.py +198 -0
- pyoframe-0.1.0/tests/test_io.py +75 -0
- pyoframe-0.1.0/tests/test_model.py +56 -0
- {pyoframe-0.0.11 → pyoframe-0.1.0}/tests/test_operations.py +8 -6
- pyoframe-0.1.0/tests/test_solver.py +214 -0
- pyoframe-0.0.11/src/pyoframe/constants.py +0 -291
- pyoframe-0.0.11/src/pyoframe/io.py +0 -252
- pyoframe-0.0.11/src/pyoframe/io_mappers.py +0 -238
- pyoframe-0.0.11/src/pyoframe/model.py +0 -116
- pyoframe-0.0.11/src/pyoframe/objective.py +0 -45
- pyoframe-0.0.11/src/pyoframe/solvers.py +0 -377
- pyoframe-0.0.11/src/pyoframe/user_defined.py +0 -60
- pyoframe-0.0.11/tests/test_examples.py +0 -157
- pyoframe-0.0.11/tests/test_io.py +0 -53
- pyoframe-0.0.11/tests/test_solver.py +0 -162
- {pyoframe-0.0.11 → pyoframe-0.1.0}/setup.cfg +0 -0
- {pyoframe-0.0.11 → pyoframe-0.1.0}/src/pyoframe.egg-info/dependency_links.txt +0 -0
- {pyoframe-0.0.11 → pyoframe-0.1.0}/src/pyoframe.egg-info/top_level.txt +0 -0
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
3
|
Copyright 2024 Bravos Power
|
|
4
|
-
Copyright 2021-2023 Fabian Hofmann
|
|
5
|
-
Copyright 2015-2021 PyPSA Developers
|
|
6
4
|
|
|
7
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
6
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pyoframe
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.1.0
|
|
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/
|
|
@@ -12,7 +12,7 @@ Classifier: Operating System :: OS Independent
|
|
|
12
12
|
Classifier: Development Status :: 3 - Alpha
|
|
13
13
|
Classifier: License :: OSI Approved :: MIT License
|
|
14
14
|
Classifier: Natural Language :: English
|
|
15
|
-
Requires-Python: >=3.
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
License-File: LICENSE
|
|
18
18
|
Requires-Dist: polars<2,>=0.20
|
|
@@ -20,8 +20,9 @@ Requires-Dist: numpy
|
|
|
20
20
|
Requires-Dist: pyarrow
|
|
21
21
|
Requires-Dist: pandas
|
|
22
22
|
Requires-Dist: packaging
|
|
23
|
+
Requires-Dist: pyoptinterface~=0.3
|
|
23
24
|
Provides-Extra: dev
|
|
24
|
-
Requires-Dist: black; extra == "dev"
|
|
25
|
+
Requires-Dist: black[jupyter]; extra == "dev"
|
|
25
26
|
Requires-Dist: bumpver; extra == "dev"
|
|
26
27
|
Requires-Dist: isort; extra == "dev"
|
|
27
28
|
Requires-Dist: pip-tools; extra == "dev"
|
|
@@ -29,6 +30,8 @@ Requires-Dist: pytest; extra == "dev"
|
|
|
29
30
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
30
31
|
Requires-Dist: pre-commit; extra == "dev"
|
|
31
32
|
Requires-Dist: gurobipy; extra == "dev"
|
|
33
|
+
Requires-Dist: highsbox; extra == "dev"
|
|
34
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
32
35
|
Provides-Extra: docs
|
|
33
36
|
Requires-Dist: mkdocs-material==9.*; extra == "docs"
|
|
34
37
|
Requires-Dist: mkdocstrings[python]; extra == "docs"
|
|
@@ -37,6 +40,9 @@ Requires-Dist: mkdocs-git-committers-plugin-2; extra == "docs"
|
|
|
37
40
|
Requires-Dist: mkdocs-gen-files; extra == "docs"
|
|
38
41
|
Requires-Dist: mkdocs-section-index; extra == "docs"
|
|
39
42
|
Requires-Dist: mkdocs-literate-nav; extra == "docs"
|
|
43
|
+
Requires-Dist: mkdocs-table-reader-plugin; extra == "docs"
|
|
44
|
+
Provides-Extra: highs
|
|
45
|
+
Requires-Dist: highsbox; extra == "highs"
|
|
40
46
|
|
|
41
47
|
# Pyoframe: Fast and low-memory linear programming models
|
|
42
48
|
|
|
@@ -50,16 +56,12 @@ Requires-Dist: mkdocs-literate-nav; extra == "docs"
|
|
|
50
56
|
|
|
51
57
|
A library to rapidly and memory-efficiently formulate large and sparse optimization models using Pandas or Polars dataframes.
|
|
52
58
|
|
|
53
|
-
##
|
|
54
|
-
|
|
55
|
-
Contributions are welcome! See [`CONTRIBUTE.md`](./CONTRIBUTE.md).
|
|
59
|
+
## **[Documentation](https://bravos-power.github.io/pyoframe/)**
|
|
56
60
|
|
|
57
|
-
|
|
61
|
+
[Read the documentation](https://bravos-power.github.io/pyoframe/) to get started or to learn how to [contribute](https://bravos-power.github.io/pyoframe/contribute/).
|
|
58
62
|
|
|
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
63
|
|
|
61
|
-
##
|
|
64
|
+
## Acknowledgments
|
|
62
65
|
|
|
63
|
-
|
|
66
|
+
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.
|
|
64
67
|
|
|
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.
|
|
@@ -10,16 +10,12 @@
|
|
|
10
10
|
|
|
11
11
|
A library to rapidly and memory-efficiently formulate large and sparse optimization models using Pandas or Polars dataframes.
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## **[Documentation](https://bravos-power.github.io/pyoframe/)**
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
[Read the documentation](https://bravos-power.github.io/pyoframe/) to get started or to learn how to [contribute](https://bravos-power.github.io/pyoframe/contribute/).
|
|
16
16
|
|
|
17
|
-
## Acknowledgments
|
|
18
|
-
|
|
19
|
-
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.
|
|
20
17
|
|
|
21
|
-
##
|
|
18
|
+
## Acknowledgments
|
|
22
19
|
|
|
23
|
-
|
|
20
|
+
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.
|
|
24
21
|
|
|
25
|
-
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.
|
|
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pyoframe"
|
|
7
|
-
version = "0.0
|
|
7
|
+
version = "0.1.0"
|
|
8
8
|
authors = [{ name = "Bravos Power", email = "dev@bravospower.com" }]
|
|
9
9
|
description = "Blazing fast linear program interface"
|
|
10
10
|
readme = "README.md"
|
|
11
|
-
requires-python = ">=3.
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
12
|
classifiers = [
|
|
13
13
|
"Programming Language :: Python :: 3",
|
|
14
14
|
"Operating System :: OS Independent",
|
|
@@ -16,11 +16,18 @@ classifiers = [
|
|
|
16
16
|
"License :: OSI Approved :: MIT License",
|
|
17
17
|
"Natural Language :: English",
|
|
18
18
|
]
|
|
19
|
-
dependencies = [
|
|
19
|
+
dependencies = [
|
|
20
|
+
"polars>=0.20,<2",
|
|
21
|
+
"numpy",
|
|
22
|
+
"pyarrow",
|
|
23
|
+
"pandas",
|
|
24
|
+
"packaging",
|
|
25
|
+
"pyoptinterface~=0.3",
|
|
26
|
+
]
|
|
20
27
|
|
|
21
28
|
[project.optional-dependencies]
|
|
22
29
|
dev = [
|
|
23
|
-
"black",
|
|
30
|
+
"black[jupyter]",
|
|
24
31
|
"bumpver",
|
|
25
32
|
"isort",
|
|
26
33
|
"pip-tools",
|
|
@@ -28,6 +35,8 @@ dev = [
|
|
|
28
35
|
"pytest-cov",
|
|
29
36
|
"pre-commit",
|
|
30
37
|
"gurobipy",
|
|
38
|
+
"highsbox",
|
|
39
|
+
"pre-commit"
|
|
31
40
|
]
|
|
32
41
|
docs = [
|
|
33
42
|
"mkdocs-material==9.*",
|
|
@@ -37,7 +46,9 @@ docs = [
|
|
|
37
46
|
"mkdocs-gen-files",
|
|
38
47
|
"mkdocs-section-index",
|
|
39
48
|
"mkdocs-literate-nav",
|
|
49
|
+
"mkdocs-table-reader-plugin"
|
|
40
50
|
]
|
|
51
|
+
highs = ["highsbox"]
|
|
41
52
|
|
|
42
53
|
[tool.isort]
|
|
43
54
|
profile = "black"
|
|
@@ -3,11 +3,10 @@ Pyoframe's public API.
|
|
|
3
3
|
Also applies the monkey patch to the DataFrame libraries.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
from pyoframe.
|
|
7
|
-
from pyoframe.core import
|
|
8
|
-
from pyoframe.constants import Config
|
|
6
|
+
from pyoframe.constants import Config, VType
|
|
7
|
+
from pyoframe.core import Constraint, Expression, Set, Variable, sum, sum_by
|
|
9
8
|
from pyoframe.model import Model
|
|
10
|
-
from pyoframe.
|
|
9
|
+
from pyoframe.monkey_patch import patch_dataframe_libraries
|
|
11
10
|
|
|
12
11
|
patch_dataframe_libraries()
|
|
13
12
|
|
|
@@ -1,19 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Defines helper functions for doing arithmetic operations on expressions (e.g. addition).
|
|
3
|
+
"""
|
|
4
|
+
|
|
1
5
|
from typing import TYPE_CHECKING, List, Optional
|
|
6
|
+
|
|
2
7
|
import polars as pl
|
|
3
8
|
|
|
4
9
|
from pyoframe.constants import (
|
|
5
10
|
COEF_KEY,
|
|
11
|
+
CONST_TERM,
|
|
12
|
+
KEY_TYPE,
|
|
13
|
+
POLARS_VERSION,
|
|
14
|
+
QUAD_VAR_KEY,
|
|
6
15
|
RESERVED_COL_KEYS,
|
|
7
16
|
VAR_KEY,
|
|
8
|
-
UnmatchedStrategy,
|
|
9
17
|
Config,
|
|
10
18
|
PyoframeError,
|
|
19
|
+
UnmatchedStrategy,
|
|
11
20
|
)
|
|
12
21
|
|
|
13
22
|
if TYPE_CHECKING: # pragma: no cover
|
|
14
23
|
from pyoframe.core import Expression
|
|
15
24
|
|
|
16
25
|
|
|
26
|
+
def _multiply_expressions(self: "Expression", other: "Expression") -> "Expression":
|
|
27
|
+
"""
|
|
28
|
+
Multiply two or more expressions together.
|
|
29
|
+
|
|
30
|
+
Examples:
|
|
31
|
+
>>> import pyoframe as pf
|
|
32
|
+
>>> m = pf.Model("min")
|
|
33
|
+
>>> m.x1 = pf.Variable()
|
|
34
|
+
>>> m.x2 = pf.Variable()
|
|
35
|
+
>>> m.x3 = pf.Variable()
|
|
36
|
+
>>> result = 5 * m.x1 * m.x2
|
|
37
|
+
>>> result
|
|
38
|
+
<Expression size=1 dimensions={} terms=1 degree=2>
|
|
39
|
+
5 x2 * x1
|
|
40
|
+
>>> result * m.x3
|
|
41
|
+
Traceback (most recent call last):
|
|
42
|
+
...
|
|
43
|
+
pyoframe.constants.PyoframeError: Failed to multiply expressions:
|
|
44
|
+
<Expression size=1 dimensions={} terms=1 degree=2> * <Expression size=1 dimensions={} terms=1>
|
|
45
|
+
Due to error:
|
|
46
|
+
Cannot multiply a quadratic expression by a non-constant.
|
|
47
|
+
"""
|
|
48
|
+
try:
|
|
49
|
+
return _multiply_expressions_core(self, other)
|
|
50
|
+
except PyoframeError as error:
|
|
51
|
+
raise PyoframeError(
|
|
52
|
+
"Failed to multiply expressions:\n"
|
|
53
|
+
+ " * ".join(
|
|
54
|
+
e.to_str(include_header=True, include_data=False) for e in [self, other]
|
|
55
|
+
)
|
|
56
|
+
+ "\nDue to error:\n"
|
|
57
|
+
+ str(error)
|
|
58
|
+
) from error
|
|
59
|
+
|
|
60
|
+
|
|
17
61
|
def _add_expressions(*expressions: "Expression") -> "Expression":
|
|
18
62
|
try:
|
|
19
63
|
return _add_expressions_core(*expressions)
|
|
@@ -28,6 +72,98 @@ def _add_expressions(*expressions: "Expression") -> "Expression":
|
|
|
28
72
|
) from error
|
|
29
73
|
|
|
30
74
|
|
|
75
|
+
def _multiply_expressions_core(self: "Expression", other: "Expression") -> "Expression":
|
|
76
|
+
self_degree, other_degree = self.degree(), other.degree()
|
|
77
|
+
if self_degree + other_degree > 2:
|
|
78
|
+
# We know one of the two must be a quadratic since 1 + 1 is not greater than 2.
|
|
79
|
+
raise PyoframeError("Cannot multiply a quadratic expression by a non-constant.")
|
|
80
|
+
if self_degree < other_degree:
|
|
81
|
+
self, other = other, self
|
|
82
|
+
self_degree, other_degree = other_degree, self_degree
|
|
83
|
+
if other_degree == 1:
|
|
84
|
+
assert (
|
|
85
|
+
self_degree == 1
|
|
86
|
+
), "This should always be true since the sum of degrees must be <=2."
|
|
87
|
+
return _quadratic_multiplication(self, other)
|
|
88
|
+
|
|
89
|
+
assert (
|
|
90
|
+
other_degree == 0
|
|
91
|
+
), "This should always be true since other cases have already been handled."
|
|
92
|
+
multiplier = other.data.drop(
|
|
93
|
+
VAR_KEY
|
|
94
|
+
) # QUAD_VAR_KEY doesn't need to be dropped since we know it doesn't exist
|
|
95
|
+
|
|
96
|
+
dims = self.dimensions_unsafe
|
|
97
|
+
other_dims = other.dimensions_unsafe
|
|
98
|
+
dims_in_common = [dim for dim in dims if dim in other_dims]
|
|
99
|
+
|
|
100
|
+
data = (
|
|
101
|
+
self.data.join(
|
|
102
|
+
multiplier,
|
|
103
|
+
on=dims_in_common if len(dims_in_common) > 0 else None,
|
|
104
|
+
how="inner" if dims_in_common else "cross",
|
|
105
|
+
)
|
|
106
|
+
.with_columns(pl.col(COEF_KEY) * pl.col(COEF_KEY + "_right"))
|
|
107
|
+
.drop(COEF_KEY + "_right")
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return self._new(data)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _quadratic_multiplication(self: "Expression", other: "Expression") -> "Expression":
|
|
114
|
+
"""
|
|
115
|
+
Multiply two expressions of degree 1.
|
|
116
|
+
|
|
117
|
+
Examples:
|
|
118
|
+
>>> import polars as pl
|
|
119
|
+
>>> df = pl.DataFrame({"dim": [1, 2, 3], "value": [1, 2, 3]})
|
|
120
|
+
>>> m = pf.Model()
|
|
121
|
+
>>> m.x1 = pf.Variable()
|
|
122
|
+
>>> m.x2 = pf.Variable()
|
|
123
|
+
>>> expr1 = df * m.x1
|
|
124
|
+
>>> expr2 = df * m.x2 * 2 + 4
|
|
125
|
+
>>> expr1 * expr2
|
|
126
|
+
<Expression size=3 dimensions={'dim': 3} terms=6 degree=2>
|
|
127
|
+
[1]: 4 x1 +2 x2 * x1
|
|
128
|
+
[2]: 8 x1 +8 x2 * x1
|
|
129
|
+
[3]: 12 x1 +18 x2 * x1
|
|
130
|
+
>>> (expr1 * expr2) - df * m.x1 * df * m.x2 * 2
|
|
131
|
+
<Expression size=3 dimensions={'dim': 3} terms=3>
|
|
132
|
+
[1]: 4 x1
|
|
133
|
+
[2]: 8 x1
|
|
134
|
+
[3]: 12 x1
|
|
135
|
+
"""
|
|
136
|
+
dims = self.dimensions_unsafe
|
|
137
|
+
other_dims = other.dimensions_unsafe
|
|
138
|
+
dims_in_common = [dim for dim in dims if dim in other_dims]
|
|
139
|
+
|
|
140
|
+
data = (
|
|
141
|
+
self.data.join(
|
|
142
|
+
other.data,
|
|
143
|
+
on=dims_in_common if len(dims_in_common) > 0 else None,
|
|
144
|
+
how="inner" if dims_in_common else "cross",
|
|
145
|
+
)
|
|
146
|
+
.with_columns(pl.col(COEF_KEY) * pl.col(COEF_KEY + "_right"))
|
|
147
|
+
.drop(COEF_KEY + "_right")
|
|
148
|
+
.rename({VAR_KEY + "_right": QUAD_VAR_KEY})
|
|
149
|
+
# Swap VAR_KEY and QUAD_VAR_KEY so that VAR_KEy is always the larger one
|
|
150
|
+
.with_columns(
|
|
151
|
+
pl.when(pl.col(VAR_KEY) < pl.col(QUAD_VAR_KEY))
|
|
152
|
+
.then(pl.col(QUAD_VAR_KEY))
|
|
153
|
+
.otherwise(pl.col(VAR_KEY))
|
|
154
|
+
.alias(VAR_KEY),
|
|
155
|
+
pl.when(pl.col(VAR_KEY) < pl.col(QUAD_VAR_KEY))
|
|
156
|
+
.then(pl.col(VAR_KEY))
|
|
157
|
+
.otherwise(pl.col(QUAD_VAR_KEY))
|
|
158
|
+
.alias(QUAD_VAR_KEY),
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
data = _sum_like_terms(data)
|
|
163
|
+
|
|
164
|
+
return self._new(data)
|
|
165
|
+
|
|
166
|
+
|
|
31
167
|
def _add_expressions_core(*expressions: "Expression") -> "Expression":
|
|
32
168
|
# Mapping of how a sum of two expressions should propogate the unmatched strategy
|
|
33
169
|
propogatation_strategies = {
|
|
@@ -116,7 +252,9 @@ def _add_expressions_core(*expressions: "Expression") -> "Expression":
|
|
|
116
252
|
not Config.disable_unmatched_checks
|
|
117
253
|
), "This code should not be reached when unmatched checks are disabled."
|
|
118
254
|
outer_join = get_indices(left).join(
|
|
119
|
-
get_indices(right),
|
|
255
|
+
get_indices(right),
|
|
256
|
+
how="full" if POLARS_VERSION.major >= 1 else "outer",
|
|
257
|
+
on=dims,
|
|
120
258
|
)
|
|
121
259
|
if outer_join.get_column(dims[0]).null_count() > 0:
|
|
122
260
|
raise PyoframeError(
|
|
@@ -159,11 +297,24 @@ def _add_expressions_core(*expressions: "Expression") -> "Expression":
|
|
|
159
297
|
propogate_strat = expressions[0].unmatched_strategy
|
|
160
298
|
expr_data = [expr.data for expr in expressions]
|
|
161
299
|
|
|
300
|
+
# Add quadratic column if it is needed and doesn't already exist
|
|
301
|
+
if any(QUAD_VAR_KEY in df.columns for df in expr_data):
|
|
302
|
+
expr_data = [
|
|
303
|
+
(
|
|
304
|
+
df.with_columns(pl.lit(CONST_TERM).alias(QUAD_VAR_KEY).cast(KEY_TYPE))
|
|
305
|
+
if QUAD_VAR_KEY not in df.columns
|
|
306
|
+
else df
|
|
307
|
+
)
|
|
308
|
+
for df in expr_data
|
|
309
|
+
]
|
|
310
|
+
|
|
162
311
|
# Sort columns to allow for concat
|
|
163
|
-
expr_data = [
|
|
312
|
+
expr_data = [
|
|
313
|
+
e.select(dims + [c for c in e.columns if c not in dims]) for e in expr_data
|
|
314
|
+
]
|
|
164
315
|
|
|
165
316
|
data = pl.concat(expr_data, how="vertical_relaxed")
|
|
166
|
-
data = data
|
|
317
|
+
data = _sum_like_terms(data)
|
|
167
318
|
|
|
168
319
|
new_expr = expressions[0]._new(data)
|
|
169
320
|
new_expr.unmatched_strategy = propogate_strat
|
|
@@ -188,6 +339,7 @@ def _add_dimension(self: "Expression", target: "Expression") -> "Expression":
|
|
|
188
339
|
return self
|
|
189
340
|
|
|
190
341
|
if not set(missing_dims) <= set(self.allowed_new_dims):
|
|
342
|
+
# TODO actually suggest using e.g. .add_dim("a", "b") instead of just "use .add_dim()"
|
|
191
343
|
raise PyoframeError(
|
|
192
344
|
f"Dataframe has missing dimensions {missing_dims}. If this is intentional, use .add_dim()\n{self.data}"
|
|
193
345
|
)
|
|
@@ -210,6 +362,20 @@ def _add_dimension(self: "Expression", target: "Expression") -> "Expression":
|
|
|
210
362
|
return self._new(result)
|
|
211
363
|
|
|
212
364
|
|
|
365
|
+
def _sum_like_terms(df: pl.DataFrame) -> pl.DataFrame:
|
|
366
|
+
"""Combines terms with the same variables. Removes quadratic column if they all happen to cancel."""
|
|
367
|
+
dims = [c for c in df.columns if c not in RESERVED_COL_KEYS]
|
|
368
|
+
var_cols = [VAR_KEY] + ([QUAD_VAR_KEY] if QUAD_VAR_KEY in df.columns else [])
|
|
369
|
+
df = (
|
|
370
|
+
df.group_by(dims + var_cols, maintain_order=True)
|
|
371
|
+
.sum()
|
|
372
|
+
.filter(pl.col(COEF_KEY) != 0)
|
|
373
|
+
)
|
|
374
|
+
if QUAD_VAR_KEY in df.columns and (df.get_column(QUAD_VAR_KEY) == CONST_TERM).all():
|
|
375
|
+
df = df.drop(QUAD_VAR_KEY)
|
|
376
|
+
return df
|
|
377
|
+
|
|
378
|
+
|
|
213
379
|
def _get_dimensions(df: pl.DataFrame) -> Optional[List[str]]:
|
|
214
380
|
"""
|
|
215
381
|
Returns the dimensions of the DataFrame. Reserved columns do not count as dimensions.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File containing shared constants used across the package.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import importlib.metadata
|
|
6
|
+
import typing
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Literal, Optional
|
|
9
|
+
|
|
10
|
+
import polars as pl
|
|
11
|
+
import pyoptinterface as poi
|
|
12
|
+
from packaging import version
|
|
13
|
+
|
|
14
|
+
# We want to try and support multiple major versions of polars
|
|
15
|
+
POLARS_VERSION = version.parse(importlib.metadata.version("polars"))
|
|
16
|
+
|
|
17
|
+
COEF_KEY = "__coeff"
|
|
18
|
+
VAR_KEY = "__variable_id"
|
|
19
|
+
QUAD_VAR_KEY = "__quadratic_variable_id"
|
|
20
|
+
CONSTRAINT_KEY = "__constraint_id"
|
|
21
|
+
SOLUTION_KEY = "solution"
|
|
22
|
+
DUAL_KEY = "dual"
|
|
23
|
+
SUPPORTED_SOLVER_TYPES = Literal["gurobi", "highs"]
|
|
24
|
+
KEY_TYPE = pl.UInt32
|
|
25
|
+
|
|
26
|
+
# Variable ID for constant terms. This variable ID is reserved.
|
|
27
|
+
CONST_TERM = 0
|
|
28
|
+
|
|
29
|
+
RESERVED_COL_KEYS = (
|
|
30
|
+
COEF_KEY,
|
|
31
|
+
VAR_KEY,
|
|
32
|
+
QUAD_VAR_KEY,
|
|
33
|
+
CONSTRAINT_KEY,
|
|
34
|
+
SOLUTION_KEY,
|
|
35
|
+
DUAL_KEY,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class _ConfigMeta(type):
|
|
40
|
+
"""Metaclass for Config that stores the default values of all configuration options."""
|
|
41
|
+
|
|
42
|
+
def __init__(cls, name, bases, dct):
|
|
43
|
+
super().__init__(name, bases, dct)
|
|
44
|
+
cls._defaults = {
|
|
45
|
+
k: v
|
|
46
|
+
for k, v in dct.items()
|
|
47
|
+
if not k.startswith("_") and type(v) != classmethod
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Config(metaclass=_ConfigMeta):
|
|
52
|
+
"""
|
|
53
|
+
Configuration options that apply to the entire library.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
default_solver: Optional[SUPPORTED_SOLVER_TYPES] = None
|
|
57
|
+
disable_unmatched_checks: bool = False
|
|
58
|
+
float_to_str_precision: Optional[int] = 5
|
|
59
|
+
print_uses_variable_names: bool = True
|
|
60
|
+
print_max_line_length: int = 80
|
|
61
|
+
print_max_lines: int = 15
|
|
62
|
+
# Number of elements to show when printing a set to the console (additional elements are replaced with ...)
|
|
63
|
+
print_max_set_elements: int = 50
|
|
64
|
+
enable_is_duplicated_expression_safety_check: bool = False
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def reset_defaults(cls):
|
|
68
|
+
"""
|
|
69
|
+
Resets all configuration options to their default values.
|
|
70
|
+
"""
|
|
71
|
+
for key, value in cls._defaults.items():
|
|
72
|
+
setattr(cls, key, value)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ConstraintSense(Enum):
|
|
76
|
+
LE = "<="
|
|
77
|
+
GE = ">="
|
|
78
|
+
EQ = "="
|
|
79
|
+
|
|
80
|
+
def to_poi(self):
|
|
81
|
+
if self == ConstraintSense.LE:
|
|
82
|
+
return poi.ConstraintSense.LessEqual
|
|
83
|
+
elif self == ConstraintSense.EQ:
|
|
84
|
+
return poi.ConstraintSense.Equal
|
|
85
|
+
elif self == ConstraintSense.GE:
|
|
86
|
+
return poi.ConstraintSense.GreaterEqual
|
|
87
|
+
else:
|
|
88
|
+
raise ValueError(f"Invalid constraint type: {self}") # pragma: no cover
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class ObjSense(Enum):
|
|
92
|
+
MIN = "min"
|
|
93
|
+
MAX = "max"
|
|
94
|
+
|
|
95
|
+
def to_poi(self):
|
|
96
|
+
if self == ObjSense.MIN:
|
|
97
|
+
return poi.ObjectiveSense.Minimize
|
|
98
|
+
elif self == ObjSense.MAX:
|
|
99
|
+
return poi.ObjectiveSense.Maximize
|
|
100
|
+
else:
|
|
101
|
+
raise ValueError(f"Invalid objective sense: {self}") # pragma: no cover
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class VType(Enum):
|
|
105
|
+
CONTINUOUS = "continuous"
|
|
106
|
+
BINARY = "binary"
|
|
107
|
+
INTEGER = "integer"
|
|
108
|
+
|
|
109
|
+
def to_poi(self):
|
|
110
|
+
if self == VType.CONTINUOUS:
|
|
111
|
+
return poi.VariableDomain.Continuous
|
|
112
|
+
elif self == VType.BINARY:
|
|
113
|
+
return poi.VariableDomain.Binary
|
|
114
|
+
elif self == VType.INTEGER:
|
|
115
|
+
return poi.VariableDomain.Integer
|
|
116
|
+
else:
|
|
117
|
+
raise ValueError(f"Invalid variable type: {self}") # pragma: no cover
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class UnmatchedStrategy(Enum):
|
|
121
|
+
UNSET = "not_set"
|
|
122
|
+
DROP = "drop"
|
|
123
|
+
KEEP = "keep"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# This is a hack to get the Literal type for VType
|
|
127
|
+
# See: https://stackoverflow.com/questions/67292470/type-hinting-enum-member-value-in-python
|
|
128
|
+
ObjSenseValue = Literal["min", "max"]
|
|
129
|
+
VTypeValue = Literal["continuous", "binary", "integer"]
|
|
130
|
+
for enum, type in [(ObjSense, ObjSenseValue), (VType, VTypeValue)]:
|
|
131
|
+
assert set(typing.get_args(type)) == {vtype.value for vtype in enum}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class PyoframeError(Exception):
|
|
135
|
+
pass
|